using DramaLing.Api.Models.Entities;
using DramaLing.Api.Services;
using Microsoft.Extensions.Logging;
using Moq;
namespace DramaLing.Api.Tests.Services;
///
/// QuestionGeneratorService 整合測試
///
public class QuestionGeneratorServiceTests : TestBase
{
private readonly QuestionGeneratorService _questionService;
private readonly OptionsVocabularyService _optionsService;
private readonly Mock> _mockQuestionLogger;
private readonly Mock> _mockOptionsLogger;
public QuestionGeneratorServiceTests()
{
_mockQuestionLogger = CreateMockLogger();
_mockOptionsLogger = CreateMockLogger();
_optionsService = new OptionsVocabularyService(DbContext, MemoryCache, _mockOptionsLogger.Object);
_questionService = new QuestionGeneratorService(DbContext, _optionsService, _mockQuestionLogger.Object);
}
#region Vocab Choice Tests
[Fact]
public async Task GenerateQuestionAsync_VocabChoice_WithSufficientVocabulary_ShouldUseSmartOptions()
{
// Arrange
await SeedTestData();
var flashcard = CreateTestFlashcard("beautiful", "B1", "adjective");
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("vocab-choice");
result.Options.Should().HaveCount(4); // 正確答案 + 3個干擾選項
result.Options.Should().Contain("beautiful");
result.CorrectAnswer.Should().Be("beautiful");
// 驗證干擾選項來自智能詞彙庫
var distractors = result.Options!.Where(o => o != "beautiful").ToList();
distractors.Should().HaveCount(3);
}
[Fact]
public async Task GenerateQuestionAsync_VocabChoice_WithInsufficientVocabulary_ShouldUseFallback()
{
// Arrange
await SeedLimitedData();
var flashcard = CreateTestFlashcard("target", "C2", "idiom"); // 使用較少見的組合
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("vocab-choice");
result.Options.Should().HaveCount(4); // 仍應該有4個選項(使用回退機制)
result.CorrectAnswer.Should().Be("target");
}
[Fact]
public async Task GenerateQuestionAsync_VocabChoice_ShouldUseFlashcardProperties()
{
// Arrange
await SeedTestData();
var flashcard = CreateTestFlashcard("computer", "B2", "noun");
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice");
// Assert
result.Should().NotBeNull();
result.CorrectAnswer.Should().Be("computer");
// 驗證使用了 Flashcard 的 DifficultyLevel 和 PartOfSpeech
// 這可以通過檢查生成的干擾選項是否符合相應的 CEFR 等級和詞性來間接驗證
}
[Fact]
public async Task GenerateQuestionAsync_VocabChoice_WithNullFlashcardProperties_ShouldUseDefaults()
{
// Arrange
await SeedTestData();
var flashcard = CreateTestFlashcard("test", null, null); // 空的屬性
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("vocab-choice");
result.CorrectAnswer.Should().Be("test");
// 應該使用預設值 B1 和 noun
}
#endregion
#region Fill Blank Tests
[Fact]
public async Task GenerateQuestionAsync_FillBlank_WithValidExample_ShouldCreateBlankedSentence()
{
// Arrange
var flashcard = CreateTestFlashcard("beautiful", "B1", "adjective", "This is a beautiful flower.");
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-fill");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("sentence-fill");
result.BlankedSentence.Should().Contain("______");
result.BlankedSentence.Should().NotContain("beautiful");
result.CorrectAnswer.Should().Be("beautiful");
result.Sentence.Should().Be("This is a beautiful flower.");
}
[Fact]
public async Task GenerateQuestionAsync_FillBlank_WithoutExample_ShouldThrowException()
{
// Arrange
var flashcard = CreateTestFlashcard("test", "B1", "noun", null);
// Act & Assert
await Assert.ThrowsAsync(
() => _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-fill"));
}
#endregion
#region Sentence Reorder Tests
[Fact]
public async Task GenerateQuestionAsync_SentenceReorder_ShouldScrambleWords()
{
// Arrange
var flashcard = CreateTestFlashcard("test", "B1", "noun", "This is a simple test sentence.");
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-reorder");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("sentence-reorder");
result.ScrambledWords.Should().NotBeNull();
result.ScrambledWords!.Length.Should().BeGreaterThan(0);
result.CorrectAnswer.Should().Be("This is a simple test sentence.");
// 驗證包含所有單字(忽略順序)
var originalWords = "This is a simple test sentence."
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Select(w => w.Trim('.', ',', '!', '?', ';', ':'));
foreach (var word in originalWords)
{
result.ScrambledWords.Should().Contain(word);
}
}
#endregion
#region Sentence Listening Tests
[Fact]
public async Task GenerateQuestionAsync_SentenceListening_ShouldProvideOptions()
{
// Arrange
await SeedFlashcardsWithExamples();
var flashcard = CreateTestFlashcard("test", "B1", "noun", "This is a test sentence.");
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-listening");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("sentence-listening");
result.Options.Should().HaveCount(4);
result.Options.Should().Contain("This is a test sentence.");
result.CorrectAnswer.Should().Be("This is a test sentence.");
result.AudioUrl.Should().Contain(flashcard.Id.ToString());
}
#endregion
#region Error Handling Tests
[Fact]
public async Task GenerateQuestionAsync_WithNonExistentFlashcard_ShouldThrowException()
{
// Act & Assert
await Assert.ThrowsAsync(
() => _questionService.GenerateQuestionAsync(Guid.NewGuid(), "vocab-choice"));
}
[Fact]
public async Task GenerateQuestionAsync_WithUnsupportedQuestionType_ShouldReturnBasicQuestion()
{
// Arrange
var flashcard = CreateTestFlashcard("test", "B1", "noun");
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "unsupported-type");
// Assert
result.Should().NotBeNull();
result.QuestionType.Should().Be("unsupported-type");
result.CorrectAnswer.Should().Be("test");
}
#endregion
#region Performance Tests
[Fact]
public async Task GenerateQuestionAsync_ShouldCompleteWithinReasonableTime()
{
// Arrange
await SeedTestData();
var flashcard = CreateTestFlashcard("performance", "B1", "noun");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Act
var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice");
// Assert
stopwatch.Stop();
result.Should().NotBeNull();
stopwatch.ElapsedMilliseconds.Should().BeLessThan(500); // 應在500ms內完成
}
#endregion
#region Helper Methods
private async Task SeedTestData()
{
var vocabularies = new[]
{
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "wonderful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "amazing", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 7, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "fantastic", CEFRLevel = "B2", PartOfSpeech = "adjective", WordLength = 9, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "excellent", CEFRLevel = "A2", PartOfSpeech = "adjective", WordLength = 9, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "computer", CEFRLevel = "B1", PartOfSpeech = "noun", WordLength = 8, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "keyboard", CEFRLevel = "B1", PartOfSpeech = "noun", WordLength = 8, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "monitor", CEFRLevel = "B2", PartOfSpeech = "noun", WordLength = 7, IsActive = true },
};
DbContext.OptionsVocabularies.AddRange(vocabularies);
await DbContext.SaveChangesAsync();
}
private async Task SeedLimitedData()
{
var vocabularies = new[]
{
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "cat", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true },
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "dog", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true }
};
DbContext.OptionsVocabularies.AddRange(vocabularies);
await DbContext.SaveChangesAsync();
}
private async Task SeedFlashcardsWithExamples()
{
var user = new User { Id = Guid.NewGuid(), Email = "test@example.com", Username = "testuser" };
DbContext.Users.Add(user);
var flashcards = new[]
{
new Flashcard
{
Id = Guid.NewGuid(),
UserId = user.Id,
Word = "example1",
Translation = "範例1",
Definition = "第一個範例",
Example = "This is the first example sentence.",
DifficultyLevel = "B1",
PartOfSpeech = "noun"
},
new Flashcard
{
Id = Guid.NewGuid(),
UserId = user.Id,
Word = "example2",
Translation = "範例2",
Definition = "第二個範例",
Example = "This is the second example sentence.",
DifficultyLevel = "B1",
PartOfSpeech = "noun"
}
};
DbContext.Flashcards.AddRange(flashcards);
await DbContext.SaveChangesAsync();
}
private Flashcard CreateTestFlashcard(string word, string? difficultyLevel, string? partOfSpeech, string? example = null)
{
var user = new User { Id = Guid.NewGuid(), Email = "test@example.com", Username = "testuser" };
DbContext.Users.Add(user);
var flashcard = new Flashcard
{
Id = Guid.NewGuid(),
UserId = user.Id,
Word = word,
Translation = $"{word} 的翻譯",
Definition = $"{word} 的定義",
Example = example,
DifficultyLevel = difficultyLevel,
PartOfSpeech = partOfSpeech
};
DbContext.Flashcards.Add(flashcard);
DbContext.SaveChanges();
return flashcard;
}
#endregion
protected override void Dispose()
{
ClearDatabase();
base.Dispose();
}
}