using GermanApp.Application.DTOs;
using GermanApp.Domain.Entities;
using GermanApp.Domain.Exceptions;
using GermanApp.Domain.Interfaces;
namespace GermanApp.Application.Services;
///
/// Application service for managing quiz questions.
/// This is part of the Application layer.
///
public class QuizQuestionService
{
private readonly IQuizQuestionRepository _quizQuestionRepository;
private readonly IQuizOptionRepository _quizOptionRepository;
private readonly IQuizRepository _quizRepository;
private readonly ILessonRepository _lessonRepository;
private readonly ProgressService _progressService;
public QuizQuestionService(
IQuizQuestionRepository quizQuestionRepository,
IQuizOptionRepository quizOptionRepository,
IQuizRepository quizRepository,
ILessonRepository lessonRepository,
ProgressService progressService)
{
_quizQuestionRepository = quizQuestionRepository;
_quizOptionRepository = quizOptionRepository;
_quizRepository = quizRepository;
_lessonRepository = lessonRepository;
_progressService = progressService;
}
///
/// Gets all quiz questions.
///
public virtual async Task> GetAllQuizQuestionsAsync(CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetAllOrderedAsync(cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets a quiz question by its ID.
///
public virtual async Task GetQuizQuestionByIdAsync(int id, CancellationToken cancellationToken = default)
{
var question = await _quizQuestionRepository.GetByIdAsync(id, cancellationToken);
return question?.ToDto();
}
///
/// Gets quiz questions by lesson ID.
///
public virtual async Task> GetQuizQuestionsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetByLessonAsync(lessonId, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets active quiz questions by lesson ID.
///
public virtual async Task> GetActiveQuizQuestionsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets a random set of quiz questions for a lesson.
///
public virtual async Task> GetRandomQuizQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetRandomQuestionsForLessonAsync(lessonId, count, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets quiz questions by difficulty level.
///
public virtual async Task> GetQuizQuestionsByDifficultyAsync(int difficulty, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetByDifficultyAsync(difficulty, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets quiz questions by type.
///
public virtual async Task> GetQuizQuestionsByTypeAsync(QuestionType type, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetByTypeAsync(type, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Creates a new quiz question with its options.
///
public virtual async Task CreateQuizQuestionAsync(CreateQuizQuestionDto dto, CancellationToken cancellationToken = default)
{
// Validate that the quiz exists
var quiz = await _quizRepository.GetByIdAsync(dto.QuizId, cancellationToken);
if (quiz == null)
throw new NotFoundException("Quiz does not exist");
// Validate question order uniqueness within the quiz
await ValidateQuestionOrderUniquenessAsync(0, dto.QuizId, dto.Order, cancellationToken);
// Create the question
var question = dto.ToEntity();
var createdQuestion = await _quizQuestionRepository.AddAsync(question, cancellationToken);
// Create options if provided
if (dto.Options != null && dto.Options.Any())
{
foreach (var optionDto in dto.Options)
{
var option = optionDto.ToEntity(createdQuestion.Id);
await _quizOptionRepository.AddAsync(option, cancellationToken);
}
}
// Reload the question with options
var questionWithOptions = await _quizQuestionRepository.GetByIdAsync(createdQuestion.Id, cancellationToken);
return questionWithOptions?.ToDto() ?? createdQuestion.ToDto();
}
///
/// Updates an existing quiz question and its options.
///
public virtual async Task UpdateQuizQuestionAsync(int id, UpdateQuizQuestionDto dto, CancellationToken cancellationToken = default)
{
var question = await _quizQuestionRepository.GetByIdAsync(id, cancellationToken);
if (question == null)
return null;
// Validate that the quiz exists
var quiz = await _quizRepository.GetByIdAsync(dto.QuizId, cancellationToken);
if (quiz == null)
throw new NotFoundException("Quiz does not exist");
// Validate question order uniqueness within the quiz
await ValidateQuestionOrderUniquenessAsync(id, dto.QuizId, dto.Order, cancellationToken);
// Update the question
question.UpdateFromDto(dto);
await _quizQuestionRepository.UpdateAsync(question, cancellationToken);
// Update options if provided
if (dto.Options != null)
{
// Delete existing options
await _quizOptionRepository.DeleteByQuestionAsync(id, cancellationToken);
// Add new options
foreach (var optionDto in dto.Options)
{
var option = new CreateQuizOptionDto(optionDto.Text, optionDto.IsCorrect, optionDto.Order).ToEntity(id);
await _quizOptionRepository.AddAsync(option, cancellationToken);
}
}
// Reload the question with updated options
var updatedQuestion = await _quizQuestionRepository.GetByIdAsync(id, cancellationToken);
return updatedQuestion?.ToDto();
}
///
/// Validates that the question order is unique within its quiz.
///
private async Task ValidateQuestionOrderUniquenessAsync(int excludeQuestionId, int quizId, int order, CancellationToken cancellationToken)
{
var exists = await _quizQuestionRepository.OrderExistsInQuizAsync(quizId, order, excludeQuestionId, cancellationToken);
if (exists)
throw new ValidationException("A quiz question with this order already exists in the specified quiz");
}
///
/// Deletes a quiz question and its options by its ID.
///
public virtual async Task DeleteQuizQuestionAsync(int id, CancellationToken cancellationToken = default)
{
var question = await _quizQuestionRepository.GetByIdAsync(id, cancellationToken);
if (question == null)
return false;
// Delete all options for this question
await _quizOptionRepository.DeleteByQuestionAsync(id, cancellationToken);
// Delete the question
await _quizQuestionRepository.DeleteAsync(question, cancellationToken);
return true;
}
///
/// Gets quiz options for a specific question.
///
public virtual async Task> GetQuizOptionsByQuestionAsync(int questionId, CancellationToken cancellationToken = default)
{
var options = await _quizOptionRepository.GetByQuestionAsync(questionId, cancellationToken);
return options.Select(o => o.ToDto()).ToList();
}
///
/// Gets the correct option for a question.
///
public virtual async Task GetCorrectOptionAsync(int questionId, CancellationToken cancellationToken = default)
{
var option = await _quizOptionRepository.GetCorrectOptionAsync(questionId, cancellationToken);
return option?.ToDto();
}
///
/// Gets all correct options for a question (for MultipleSelect type).
///
public virtual async Task> GetCorrectOptionsAsync(int questionId, CancellationToken cancellationToken = default)
{
var options = await _quizOptionRepository.GetCorrectOptionsAsync(questionId, cancellationToken);
return options.Select(o => o.ToDto()).ToList();
}
///
/// Creates a quiz option for a question.
///
public virtual async Task CreateQuizOptionAsync(int questionId, CreateQuizOptionDto dto, CancellationToken cancellationToken = default)
{
// Validate that the question exists
var question = await _quizQuestionRepository.GetByIdAsync(questionId, cancellationToken);
if (question == null)
throw new ArgumentException("Quiz question does not exist");
// Validate option order uniqueness within the question
await ValidateOptionOrderUniquenessAsync(questionId, dto.Order, null, cancellationToken);
var option = dto.ToEntity(questionId);
var createdOption = await _quizOptionRepository.AddAsync(option, cancellationToken);
return createdOption.ToDto();
}
///
/// Updates an existing quiz option.
///
public virtual async Task UpdateQuizOptionAsync(int id, UpdateQuizOptionDto dto, CancellationToken cancellationToken = default)
{
var option = await _quizOptionRepository.GetByIdAsync(id, cancellationToken);
if (option == null)
return null;
// Validate that the question exists
var question = await _quizQuestionRepository.GetByIdAsync(dto.Id, cancellationToken);
if (question == null)
throw new ArgumentException("Quiz question does not exist");
// Validate option order uniqueness within the question
await ValidateOptionOrderUniquenessAsync(option.QuizQuestionId, dto.Order, id, cancellationToken);
option.UpdateFromDto(dto);
await _quizOptionRepository.UpdateAsync(option, cancellationToken);
return option.ToDto();
}
///
/// Validates that the option order is unique within its question.
///
private async Task ValidateOptionOrderUniquenessAsync(int questionId, int order, int? excludeOptionId, CancellationToken cancellationToken)
{
var exists = await _quizOptionRepository.OrderExistsForQuestionAsync(questionId, order, excludeOptionId, cancellationToken);
if (exists)
throw new ArgumentException("An option with this order already exists for the specified question");
}
///
/// Deletes a quiz option by its ID.
///
public virtual async Task DeleteQuizOptionAsync(int id, CancellationToken cancellationToken = default)
{
var option = await _quizOptionRepository.GetByIdAsync(id, cancellationToken);
if (option == null)
return false;
await _quizOptionRepository.DeleteAsync(option, cancellationToken);
return true;
}
///
/// Submits quiz answers for a specific quiz and returns the result.
///
public virtual async Task SubmitQuizAnswersForQuizAsync(int userId, SubmitQuizDto dto, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetWithQuestionsAsync(dto.QuizId, cancellationToken);
if (quiz == null)
throw new NotFoundException("Quiz does not exist");
var questions = quiz.Questions?.Where(q => q.IsActive).OrderBy(q => q.Order).ToList() ?? new List();
var totalQuestions = questions.Count;
var totalPoints = questions.Sum(q => q.Points);
var scorePoints = 0;
var questionResults = new List();
// Get user's answers (using QuizAnswerDto which has the same fields as SubmitAnswerDto)
var userAnswers = dto.Answers.ToDictionary(a => a.QuizQuestionId);
foreach (var question in questions)
{
var isCorrect = false;
var userAnswerText = string.Empty;
var correctAnswer = question.CorrectAnswer ?? string.Empty;
if (userAnswers.TryGetValue(question.Id, out var userAnswer))
{
userAnswerText = userAnswer.AnswerText ?? string.Empty;
switch (question.Type)
{
case QuestionType.MultipleChoice:
// Check if the selected option is correct
if (userAnswer.SelectedOptionIds != null && userAnswer.SelectedOptionIds.Length == 1)
{
var selectedOptionId = userAnswer.SelectedOptionIds[0];
var isOptionCorrect = await _quizOptionRepository.GetByIdAsync(selectedOptionId, cancellationToken);
isCorrect = isOptionCorrect?.IsCorrect == true;
}
break;
case QuestionType.MultipleSelect:
// Check if all selected options are correct and no incorrect ones are selected
if (userAnswer.SelectedOptionIds != null && userAnswer.SelectedOptionIds.Length > 0)
{
var correctOptions = await _quizOptionRepository.GetCorrectOptionsAsync(question.Id, cancellationToken);
var selectedOptions = await _quizOptionRepository.GetByQuestionAsync(question.Id, cancellationToken);
var correctIds = new HashSet(correctOptions.Select(o => o.Id));
var selectedIds = new HashSet(userAnswer.SelectedOptionIds);
// All selected must be correct, and all correct must be selected
isCorrect = selectedIds.SetEquals(correctIds);
}
break;
case QuestionType.TrueFalse:
// Check if the answer matches
if (userAnswer.AnswerText != null)
{
isCorrect = userAnswer.AnswerText.Equals(correctAnswer, StringComparison.OrdinalIgnoreCase);
}
break;
case QuestionType.FillInTheBlank:
case QuestionType.ShortAnswer:
// Case-insensitive comparison with trimming
if (!string.IsNullOrEmpty(userAnswerText) && !string.IsNullOrEmpty(correctAnswer))
{
isCorrect = userAnswerText.Trim().Equals(correctAnswer.Trim(), StringComparison.OrdinalIgnoreCase);
}
break;
case QuestionType.Matching:
// For matching, we would need more complex logic
// This is a simplified implementation
break;
}
}
if (isCorrect)
scorePoints += question.Points;
questionResults.Add(new QuizQuestionResultDto(
question.Id,
question.QuestionText,
question.Type,
question.Points,
isCorrect,
correctAnswer,
userAnswerText));
}
var scorePercentage = totalPoints > 0 ? (double)scorePoints / totalPoints * 100 : 0;
var passed = quiz.IsPassed(scorePercentage);
// Update user progress if quiz is passed
if (passed && userId > 0)
{
try
{
await _progressService.MarkLessonAsCompletedAsync(
userId,
quiz.LessonId,
(int)scorePercentage,
cancellationToken);
}
catch
{
// Log error but don't fail the quiz submission
// In production, you would use a logger here
// For now, we'll just swallow the exception to keep the quiz submission working
// Consider adding logging: _logger.LogError(ex, "Failed to update progress for user {UserId} lesson {LessonId}", userId, quiz.LessonId);
}
}
return new QuizResultDto(
quiz.Id,
quiz.LessonId,
quiz.Title,
totalQuestions,
totalPoints,
scorePoints,
scorePercentage,
passed,
TimeSpan.Zero, // Would be calculated based on actual time taken
questionResults);
}
///
/// Submits quiz answers and returns the result (legacy - converts SubmitQuizAnswersDto to SubmitQuizDto).
///
public virtual async Task SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
{
// Convert legacy DTO to new format
// SubmitQuizAnswersDto has QuizId and Answers (IReadOnlyList)
// SubmitQuizDto has QuizId and Answers (IReadOnlyList)
// They're the same, so just create a new SubmitQuizDto
var newDto = new SubmitQuizDto(dto.QuizId, dto.Answers);
return await SubmitQuizAnswersForQuizAsync(userId, newDto, cancellationToken);
}
///
/// Gets the number of quiz questions for a lesson.
///
public virtual async Task GetQuizQuestionCountForLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
return questions.Count;
}
///
/// Gets the difficulty distribution for a lesson.
///
public virtual async Task> GetDifficultyDistributionAsync(int lessonId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
var distribution = new Dictionary();
for (int i = 1; i <= 5; i++)
{
distribution[i] = questions.Count(q => q.Difficulty == i);
}
return distribution;
}
///
/// Gets the type distribution for a lesson.
///
public virtual async Task> GetTypeDistributionAsync(int lessonId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
var distribution = new Dictionary();
foreach (var type in Enum.GetValues(typeof(QuestionType)).Cast())
{
distribution[type.ToString()] = questions.Count(q => q.Type == type);
}
return distribution;
}
// ============================================
// Quiz-based methods (QuizId)
// ============================================
///
/// Gets quiz questions by quiz ID.
///
public virtual async Task> GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetByQuizAsync(quizId, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets active quiz questions by quiz ID.
///
public virtual async Task> GetActiveQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetActiveByQuizAsync(quizId, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets a random set of quiz questions for a quiz.
///
public virtual async Task> GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionRepository.GetRandomQuestionsForQuizAsync(quizId, count, cancellationToken);
return questions.Select(q => q.ToDto()).ToList();
}
///
/// Gets the total points for a quiz.
///
public virtual async Task GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default)
{
return await _quizQuestionRepository.GetTotalPointsForQuizAsync(quizId, cancellationToken);
}
}