using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GermanApp.Application.DTOs; using GermanApp.Application.Services; using GermanApp.Domain.Entities; using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace GermanApp.Tests.Unit.Application.Services; [TestClass] public class StoryGenerationServiceTests { private Mock _mockMistralService; private Mock _mockStoryRepository; private Mock _mockTtsService; private Mock> _mockLogger; private StoryGenerationService _service; [TestInitialize] public void Setup() { _mockMistralService = new Mock(); _mockStoryRepository = new Mock(); _mockTtsService = new Mock(); _mockLogger = new Mock>(); _service = new StoryGenerationService( _mockMistralService.Object, _mockStoryRepository.Object, _mockTtsService.Object, _mockLogger.Object); } private Lesson CreateTestLesson(int id, int levelId, string title, string topic, int order) { var lesson = Lesson.Create(levelId, title, order, topic); // Use reflection to set the Id since it's private set typeof(Lesson).GetProperty("Id")?.SetValue(lesson, id); return lesson; } private StorySegment CreateTestSegment(int id, int levelId, int? lessonId, string content, int order, string title, string theme) { var segment = StorySegment.Create(levelId, lessonId, content, order, title, theme); typeof(StorySegment).GetProperty("Id")?.SetValue(segment, id); return segment; } #region GenerateStoryAsync Tests [TestMethod] public async Task GenerateStoryAsync_WithValidData_ReturnsResponse() { // Arrange var levelId = 1; var theme = "Abenteuer"; var lesson1 = CreateTestLesson(1, 1, "Lektion 1", "Einfuehrung", 1); var lesson2 = CreateTestLesson(2, 1, "Lektion 2", "Fortsetzung", 2); var lessons = new List { lesson1, lesson2 }; var expectedStory = "Es war einmal ein Abenteuer..."; _mockMistralService.Setup(m => m.GenerateStoryAsync( "A1", theme, It.IsAny>(), 500, It.IsAny())) .ReturnsAsync(expectedStory); _mockStoryRepository.Setup(s => s.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((StorySegment s, CancellationToken ct) => s); // Act var result = await _service.GenerateStoryAsync(levelId, theme, lessons); // Assert Assert.IsNotNull(result); Assert.AreEqual(levelId, result.LevelId); Assert.AreEqual(theme, result.Theme); Assert.AreEqual(2, result.SegmentCount); Assert.AreEqual(expectedStory, result.FullStoryText); Assert.AreEqual(2, result.Segments.Count); _mockMistralService.Verify(m => m.GenerateStoryAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] public async Task GenerateStoryAsync_WithNoVocabulary_ThrowsException() { // Arrange var levelId = 1; var theme = "Abenteuer"; var lessons = new List(); // Empty list = no vocabulary // Act & Assert try { await _service.GenerateStoryAsync(levelId, theme, lessons); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException ex) { Assert.IsTrue(ex.Message.Contains("No vocabulary")); } } [TestMethod] public async Task GenerateStoryAsync_WithEmptyResponse_ThrowsException() { // Arrange var levelId = 1; var theme = "Abenteuer"; var lesson1 = CreateTestLesson(1, 1, "Lektion 1", "Test", 1); var lessons = new List { lesson1 }; _mockMistralService.Setup(m => m.GenerateStoryAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(string.Empty); // Act & Assert try { await _service.GenerateStoryAsync(levelId, theme, lessons); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException ex) { Assert.IsTrue(ex.Message.Contains("Empty response")); } } [TestMethod] public async Task GenerateStoryAsync_WithNullResponse_ThrowsException() { // Arrange var levelId = 1; var theme = "Abenteuer"; var lesson1 = CreateTestLesson(1, 1, "Lektion 1", "Test", 1); var lessons = new List { lesson1 }; _mockMistralService.Setup(m => m.GenerateStoryAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync((string?)null); // Act & Assert try { await _service.GenerateStoryAsync(levelId, theme, lessons); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException ex) { Assert.IsTrue(ex.Message.Contains("Empty response")); } } [TestMethod] public async Task GenerateStoryAsync_WithLevelA2_UsesCorrectLevelCode() { // Arrange var levelId = 2; var theme = "Abenteuer"; var lesson1 = CreateTestLesson(1, 2, "Lektion 1", "Test", 1); var lessons = new List { lesson1 }; var expectedStory = "A2 Story"; _mockMistralService.Setup(m => m.GenerateStoryAsync( "A2", theme, It.IsAny>(), 500, It.IsAny())) .ReturnsAsync(expectedStory); _mockStoryRepository.Setup(s => s.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((StorySegment s, CancellationToken ct) => s); // Act var result = await _service.GenerateStoryAsync(levelId, theme, lessons); // Assert Assert.IsNotNull(result); _mockMistralService.Verify(m => m.GenerateStoryAsync( "A2", It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] public async Task GenerateStoryAsync_WithLevelB1_UsesCorrectLevelCode() { // Arrange var levelId = 3; var theme = "Abenteuer"; var lesson1 = CreateTestLesson(1, 3, "Lektion 1", "Test", 1); var lessons = new List { lesson1 }; var expectedStory = "B1 Story"; _mockMistralService.Setup(m => m.GenerateStoryAsync( "B1", theme, It.IsAny>(), 500, It.IsAny())) .ReturnsAsync(expectedStory); _mockStoryRepository.Setup(s => s.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((StorySegment s, CancellationToken ct) => s); // Act var result = await _service.GenerateStoryAsync(levelId, theme, lessons); // Assert Assert.IsNotNull(result); _mockMistralService.Verify(m => m.GenerateStoryAsync( "B1", It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); } #endregion #region GenerateSegmentAsync Tests [TestMethod] public async Task GenerateSegmentAsync_WithValidData_ReturnsSegment() { // Arrange var levelId = 1; var lessonId = 1; var theme = "Abenteuer"; var vocabulary = new List { "Haus", "Hund", "Katze" }; var order = 1; var expectedContent = "Ein kurzer Text..."; _mockMistralService.Setup(m => m.GenerateStoryAsync( "A1", theme, vocabulary, 100, It.IsAny())) .ReturnsAsync(expectedContent); var createdSegment = CreateTestSegment(100, levelId, lessonId, expectedContent, order, $"{theme} - Part {order}", theme); _mockStoryRepository.Setup(s => s.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(createdSegment); // Act var result = await _service.GenerateSegmentAsync(levelId, lessonId, theme, vocabulary, order); // Assert Assert.IsNotNull(result); Assert.AreEqual(100, result.Id); Assert.AreEqual(expectedContent, result.Content); Assert.AreEqual($"{theme} - Part {order}", result.Title); _mockMistralService.Verify(m => m.GenerateStoryAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] public async Task GenerateSegmentAsync_WithNoVocabulary_ThrowsException() { // Arrange var levelId = 1; var lessonId = 1; var theme = "Abenteuer"; var vocabulary = new List(); var order = 1; // Act & Assert try { await _service.GenerateSegmentAsync(levelId, lessonId, theme, vocabulary, order); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException ex) { Assert.IsTrue(ex.Message.Contains("No vocabulary")); } } [TestMethod] public async Task GenerateSegmentAsync_WithEmptyResponse_ThrowsException() { // Arrange var levelId = 1; var lessonId = 1; var theme = "Abenteuer"; var vocabulary = new List { "Test" }; var order = 1; _mockMistralService.Setup(m => m.GenerateStoryAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(string.Empty); // Act & Assert try { await _service.GenerateSegmentAsync(levelId, lessonId, theme, vocabulary, order); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException ex) { Assert.IsTrue(ex.Message.Contains("Empty response")); } } #endregion #region GenerateAudioAsync Tests [TestMethod] public async Task GenerateAudioAsync_WithExistingSegmentAndNoAudio_GeneratesAudio() { // Arrange var segmentId = 1; var segment = CreateTestSegment(segmentId, 1, 1, "Test content", 1, "Test Title", "Test Theme"); _mockStoryRepository.Setup(s => s.GetByIdAsync(segmentId, It.IsAny())) .ReturnsAsync(segment); _mockTtsService.Setup(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("/audio/story/level1-segment1.wav"); _mockStoryRepository.Setup(s => s.UpdateAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Act var result = await _service.GenerateAudioAsync(segmentId); // Assert Assert.IsNotNull(result); Assert.AreEqual("/audio/story/level1-segment1.wav", result.AudioUrl); _mockTtsService.Verify(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] public async Task GenerateAudioAsync_WithNonExistingSegment_ReturnsNull() { // Arrange var segmentId = 999; _mockStoryRepository.Setup(s => s.GetByIdAsync(segmentId, It.IsAny())) .ReturnsAsync((StorySegment?)null); // Act var result = await _service.GenerateAudioAsync(segmentId); // Assert Assert.IsNull(result); } [TestMethod] public async Task GenerateAudioAsync_WithExistingAudio_ReturnsExisting() { // Arrange var segmentId = 1; var segment = CreateTestSegment(segmentId, 1, 1, "Test content", 1, "Test Title", "Test Theme"); segment.UpdateAudioUrl("/audio/existing.wav"); _mockStoryRepository.Setup(s => s.GetByIdAsync(segmentId, It.IsAny())) .ReturnsAsync(segment); // Act var result = await _service.GenerateAudioAsync(segmentId); // Assert Assert.IsNotNull(result); Assert.AreEqual("/audio/existing.wav", result.AudioUrl); _mockTtsService.Verify(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region GenerateAudioForAllSegmentsAsync Tests [TestMethod] public async Task GenerateAudioForAllSegmentsAsync_WithSegmentsNeedingAudio_GeneratesAll() { // Arrange var segment1 = CreateTestSegment(1, 1, 1, "Content 1", 1, "Title 1", "Theme"); var segment2 = CreateTestSegment(2, 1, 2, "Content 2", 2, "Title 2", "Theme"); _mockStoryRepository.Setup(s => s.GetSegmentsNeedingAudioAsync(It.IsAny())) .ReturnsAsync(new List { segment1, segment2 }); _mockStoryRepository.Setup(s => s.GetByIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((int id, CancellationToken ct) => id == 1 ? segment1 : segment2); _mockTtsService.Setup(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string text, string outputPath, string? speaker, string language, CancellationToken ct) => outputPath); _mockStoryRepository.Setup(s => s.UpdateAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Act var results = await _service.GenerateAudioForAllSegmentsAsync(); // Assert Assert.IsNotNull(results); Assert.AreEqual(2, results.Count); _mockTtsService.Verify(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [TestMethod] public async Task GenerateAudioForAllSegmentsAsync_WithLevelFilter_FiltersByLevel() { // Arrange var segment1 = CreateTestSegment(1, 1, 1, "Content 1", 1, "Title 1", "Theme"); var segment2 = CreateTestSegment(2, 2, 1, "Content 2", 1, "Title 2", "Theme"); _mockStoryRepository.Setup(s => s.GetSegmentsNeedingAudioAsync(It.IsAny())) .ReturnsAsync(new List { segment1, segment2 }); _mockStoryRepository.Setup(s => s.GetByIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((int id, CancellationToken ct) => id == 1 ? segment1 : segment2); _mockTtsService.Setup(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string text, string outputPath, string? speaker, string language, CancellationToken ct) => outputPath); _mockStoryRepository.Setup(s => s.UpdateAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Act var results = await _service.GenerateAudioForAllSegmentsAsync(levelId: 1); // Assert Assert.IsNotNull(results); Assert.AreEqual(1, results.Count); Assert.AreEqual(1, results[0].LevelId); } [TestMethod] public async Task GenerateAudioForAllSegmentsAsync_WithNoSegments_ReturnsEmpty() { // Arrange _mockStoryRepository.Setup(s => s.GetSegmentsNeedingAudioAsync(It.IsAny())) .ReturnsAsync(new List()); // Act var results = await _service.GenerateAudioForAllSegmentsAsync(); // Assert Assert.IsNotNull(results); Assert.AreEqual(0, results.Count); } [TestMethod] public async Task GenerateAudioForAllSegmentsAsync_WhenOneFails_ContinuesWithOthers() { // Arrange var segment1 = CreateTestSegment(1, 1, 1, "Content 1", 1, "Title 1", "Theme"); var segment2 = CreateTestSegment(2, 1, 2, "Content 2", 2, "Title 2", "Theme"); _mockStoryRepository.Setup(s => s.GetSegmentsNeedingAudioAsync(It.IsAny())) .ReturnsAsync(new List { segment1, segment2 }); _mockStoryRepository.Setup(s => s.GetByIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((int id, CancellationToken ct) => id == 1 ? segment1 : segment2); // First call succeeds, second throws _mockTtsService.SetupSequence(t => t.GenerateAudioToFileAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("/audio/test1.wav") .ThrowsAsync(new Exception("TTS Error")); _mockStoryRepository.Setup(s => s.UpdateAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Act var results = await _service.GenerateAudioForAllSegmentsAsync(); // Assert Assert.IsNotNull(results); Assert.AreEqual(1, results.Count); // Only first one succeeded Assert.AreEqual(1, results[0].Id); } #endregion }