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; public QuizQuestionService( IQuizQuestionRepository quizQuestionRepository, IQuizOptionRepository quizOptionRepository, IQuizRepository quizRepository, ILessonRepository lessonRepository) { _quizQuestionRepository = quizQuestionRepository; _quizOptionRepository = quizOptionRepository; _quizRepository = quizRepository; _lessonRepository = lessonRepository; } /// /// 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); 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); } }