using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels; using LehrerApp.Desktop.ViewModels.Groups; using Xunit; namespace LehrerApp.Desktop.Tests; /// Tests für die Kurs-Übersicht: sie liest bewusst keine eigenen Daten, sondern zieht nur /// zusammen, was Planung/Klausuren/Mitarbeit/Dokumentation ohnehin schon verwalten. public sealed class GroupOverviewViewModelTests { [Fact] public void UnterrichtHeute_BietetNurHeutigeNichtAusgefalleneStundenUndOeffnetAuswahl() { var group = new LearningGroup { IsActive = true }; var today = DateOnly.FromDateTime(DateTime.Today); var lessons = new FakeLessons(); var first = new Lesson { GroupId = group.Id, Date = today, Topic = "Erste Stunde" }; var second = new Lesson { GroupId = group.Id, Date = today, Topic = "Zweite Stunde" }; lessons.Add(first); lessons.Add(second); lessons.Add(new Lesson { GroupId = group.Id, Date = today, Status = LessonStatus.Cancelled }); lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(1) }); lessons.Add(new Lesson { GroupId = Guid.NewGuid(), Date = today }); var vm = NewVm(lessons: lessons, groups: new FakeGroups([group])); vm.Initialize(group.Id, group.Name); Assert.True(vm.HasTodayLessons); Assert.Equal(2, vm.TodayLessons.Count); Lesson? opened = null; vm.OnOpenTeachingMode = lesson => opened = lesson; vm.SelectedTeachingLesson = second; vm.StartTeachingModeCommand.Execute(null); Assert.Same(second, opened); group.IsActive = false; vm.Refresh(); Assert.False(vm.HasTodayLessons); } private static GroupOverviewViewModel NewVm(FakeLessons? lessons = null, FakeExams? exams = null, FakeSessions? sessions = null, FakeEntries? entries = null, FakeStudents? students = null, FakeDocumentation? documentation = null, FakeWorkTasks? tasks = null, FakeResults? results = null, FakeGroups? groups = null) => new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), results ?? new FakeResults(), sessions ?? new FakeSessions([]), entries ?? new FakeEntries(), students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(), tasks ?? new FakeWorkTasks(), groups ?? new FakeGroups([]), new AttendanceBalanceService(), new GradingService(), new SchoolYearService()); private static (GroupOverviewViewModel Vm, FakeLessons Lessons, FakeExams Exams, FakeSessions Sessions, FakeEntries Entries, FakeStudents Students, Guid GroupId) BuildScenario( List? lessons = null, List? exams = null) { var groupId = Guid.NewGuid(); var lessonsRepo = new FakeLessons(); foreach (var l in lessons ?? []) lessonsRepo.Add(l); var examsRepo = new FakeExams(exams ?? []); var sessions = new FakeSessions([]); var entries = new FakeEntries(); var students = new FakeStudents([]); var vm = NewVm(lessonsRepo, examsRepo, sessions, entries, students); vm.Initialize(groupId, "Testkurs"); return (vm, lessonsRepo, examsRepo, sessions, entries, students, groupId); } [Fact] public void NaechsteStunde_FindetDieNaechsteGeplanteStundeInnerhalbDesZeitfensters() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(10), Topic = "Später", Status = LessonStatus.Planned }); lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(3), Topic = "Brechung", Status = LessonStatus.Planned }); lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(1), Topic = "Schon vorbei", Status = LessonStatus.Conducted }); var vm = NewVm(lessons: lessons); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.HasNextLesson); Assert.Contains("Brechung", vm.NextLessonLabel); } [Fact] public void NaechsteStunde_OhneGeplanteStundeIstNichtVorhanden() { var (vm, _, _, _, _, _, _) = BuildScenario(); Assert.False(vm.HasNextLesson); } [Fact] public void NaechsteKlausur_FindetFruehesteKuenftigeKlausur() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var exams = new FakeExams([ new Exam { GroupId = groupId, Date = today.AddDays(20), Title = "Später" }, new Exam { GroupId = groupId, Date = today.AddDays(5), Title = "Klassenarbeit 2" }, new Exam { GroupId = groupId, Date = today.AddDays(-3), Title = "Vergangen" }, ]); var vm = NewVm(exams: exams); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.HasNextExam); Assert.Contains("Klassenarbeit 2", vm.NextExamLabel); } [Fact] public void Vorjahresvergleich_OhneVerknuepfteVorgaengergruppe_BleibtVerborgen() { var groupId = Guid.NewGuid(); var groups = new FakeGroups([new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6 }]); var vm = NewVm(groups: groups); vm.Initialize(groupId, "Testkurs"); Assert.False(vm.HasYearComparison); } [Fact] public void Vorjahresvergleich_VorgaengerOhneKlausurergebnisse_BleibtVerborgen() { var previousId = Guid.NewGuid(); var groupId = Guid.NewGuid(); var groups = new FakeGroups([ new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6, PreviousGroupId = previousId }, new LearningGroup { Id = previousId, GradingSystem = GradingSystem.Grades1To6 }, ]); var vm = NewVm(groups: groups); vm.Initialize(groupId, "Testkurs"); Assert.False(vm.HasYearComparison); } [Fact] public void Vorjahresvergleich_ZeigtDurchschnitteBeiderJahreAn() { var previousId = Guid.NewGuid(); var groupId = Guid.NewGuid(); var groups = new FakeGroups([ new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6, PreviousGroupId = previousId }, new LearningGroup { Id = previousId, GradingSystem = GradingSystem.Grades1To6 }, ]); var previousExam = new Exam { Id = Guid.NewGuid(), GroupId = previousId }; var currentExam = new Exam { Id = Guid.NewGuid(), GroupId = groupId }; var exams = new FakeExams([previousExam, currentExam]); var results = new FakeResults(); results.Add(new ExamResult { ExamId = previousExam.Id, StudentId = Guid.NewGuid(), Grade = "2" }); results.Add(new ExamResult { ExamId = previousExam.Id, StudentId = Guid.NewGuid(), Grade = "4" }); results.Add(new ExamResult { ExamId = currentExam.Id, StudentId = Guid.NewGuid(), Grade = "1" }); // Abwesende und leere Ergebnisse fließen bewusst nicht in den Schnitt ein. results.Add(new ExamResult { ExamId = currentExam.Id, StudentId = Guid.NewGuid(), Absent = true, Grade = "6" }); var vm = NewVm(exams: exams, results: results, groups: groups); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.HasYearComparison); Assert.Contains("Dieses Jahr: Ø 1,0 (1 Klausuren)", vm.YearComparisonLabel); Assert.Contains("Vorjahr: Ø 3,0 (2 Klausuren)", vm.YearComparisonLabel); } [Fact] public void Vorjahresvergleich_OhneEigeneKlausurenZeigtNurVorjahr() { var previousId = Guid.NewGuid(); var groupId = Guid.NewGuid(); var groups = new FakeGroups([ new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6, PreviousGroupId = previousId }, new LearningGroup { Id = previousId, GradingSystem = GradingSystem.Grades1To6 }, ]); var previousExam = new Exam { Id = Guid.NewGuid(), GroupId = previousId }; var exams = new FakeExams([previousExam]); var results = new FakeResults(); results.Add(new ExamResult { ExamId = previousExam.Id, StudentId = Guid.NewGuid(), Grade = "3" }); var vm = NewVm(exams: exams, results: results, groups: groups); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.HasYearComparison); Assert.Contains("noch keine Klausur", vm.YearComparisonLabel); } [Fact] public void Mitarbeit_OhneSitzungenZeigtHinweisOhneWarnung() { var (vm, _, _, _, _, _, _) = BuildScenario(); Assert.Contains("Noch keine Mitarbeitssitzung", vm.ParticipationHintLabel); Assert.False(vm.ParticipationHintIsStale); } [Fact] public void Mitarbeit_LetzteSitzungVorMehrAls14TagenGiltAlsVeraltet() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var sessions = new FakeSessions([new ParticipationSession { GroupId = groupId, Date = today.AddDays(-20) }]); var vm = NewVm(sessions: sessions); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.ParticipationHintIsStale); } [Fact] public void Mitarbeit_LetzteSitzungKuerzlichGiltNichtAlsVeraltet() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var sessions = new FakeSessions([new ParticipationSession { GroupId = groupId, Date = today.AddDays(-2) }]); var vm = NewVm(sessions: sessions); vm.Initialize(groupId, "Testkurs"); Assert.False(vm.ParticipationHintIsStale); } [Fact] public void OffeneHausaufgabenKontrolle_ZeigtUnkontrollierteHausaufgabeDerLetztenStunde() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(-2), Homework = "S. 42 Nr. 3", HomeworkChecked = false, HomeworkCheckDismissed = false, }); var vm = NewVm(lessons: lessons); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.HasOpenHomeworkCheck); Assert.Contains("S. 42 Nr. 3", vm.OpenHomeworkCheckLabel); } [Fact] public void OffeneHausaufgabenKontrolle_VerschwindetSobaldKontrolliert() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = groupId, Date = today.AddDays(-2), Homework = "S. 42 Nr. 3", HomeworkChecked = true, }); var vm = NewVm(lessons: lessons); vm.Initialize(groupId, "Testkurs"); Assert.False(vm.HasOpenHomeworkCheck); } [Fact] public void OffeneEntschuldigungen_ListetOffenePendenteEntschuldigungenUndErlaubtAufloesen() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var student = new Student { FirstName = "Anna", LastName = "Beispiel" }; var session = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-2) }; var sessions = new FakeSessions([session]); var entries = new FakeEntries(); var entry = new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = AttendanceStatus.ExcusePending }; entries.Add(entry); var students = new FakeStudents([student]); var vm = NewVm(sessions: sessions, entries: entries, students: students); vm.Initialize(groupId, "Testkurs"); var item = Assert.Single(vm.OpenExcuses); Assert.Equal(student.FullName, item.StudentName); item.MarkExcusedCommand.Execute(null); Assert.Empty(vm.OpenExcuses); Assert.Equal(AttendanceStatus.Excused, entries.GetBySessionAndStudent(session.Id, student.Id)!.Attendance); } [Fact] public void OffeneDokumentationen_ZaehltNurEntwuerfeDieserGruppeOderOhneGruppe() { var groupId = Guid.NewGuid(); var otherGroupId = Guid.NewGuid(); var student = new Student { FirstName = "Anna", LastName = "Beispiel" }; var students = new FakeStudents([student]); var documentation = new FakeDocumentation(); documentation.Add(new Documentation { StudentId = student.Id, GroupId = groupId, IsDraft = true, Title = "Beobachtung" }); documentation.Add(new Documentation { StudentId = student.Id, GroupId = null, IsDraft = true, Title = "Ohne Gruppe" }); documentation.Add(new Documentation { StudentId = student.Id, GroupId = otherGroupId, IsDraft = true, Title = "Anderer Kurs" }); documentation.Add(new Documentation { StudentId = student.Id, GroupId = groupId, IsDraft = false, Title = "Fertig" }); var vm = NewVm(students: students, documentation: documentation); vm.Initialize(groupId, "Testkurs"); Assert.Equal(2, vm.DraftDocumentationCount); } [Fact] public void AuffaelligeFehlzeiten_WirdErstAbMindeststichprobeGemeldet() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var student = new Student { FirstName = "Anna", LastName = "Beispiel" }; var students = new FakeStudents([student]); var sessions = new FakeSessions([]); var entries = new FakeEntries(); // Nur 3 erfasste Termine, davon 3 unentschuldigt (100 %) — trotz hoher Quote noch keine // Meldung, weil die Stichprobe zu klein ist (Schuljahresbeginn-Problem). for (var i = 0; i < 3; i++) { var session = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-i - 1) }; sessions.Save(session); entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = AttendanceStatus.Unexcused }); } var vm = NewVm(sessions: sessions, entries: entries, students: students); vm.Initialize(groupId, "Testkurs"); Assert.False(vm.HasAttendanceNotices); } [Fact] public void AuffaelligeFehlzeiten_MeldetHoheQuoteAbMindeststichprobe() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var student = new Student { FirstName = "Anna", LastName = "Beispiel" }; var students = new FakeStudents([student]); var sessions = new FakeSessions([]); var entries = new FakeEntries(); // 10 erfasste Termine, 3 davon unentschuldigt (30 % > 20 % Schwelle) — jetzt genug // Stichprobe für eine Meldung. for (var i = 0; i < 10; i++) { var session = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-i - 1) }; sessions.Save(session); var status = i < 3 ? AttendanceStatus.Unexcused : AttendanceStatus.Present; entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = status }); } var vm = NewVm(sessions: sessions, entries: entries, students: students); vm.Initialize(groupId, "Testkurs"); var item = Assert.Single(vm.AttendanceNotices); Assert.Equal(student.FullName, item.StudentName); Assert.Equal(30.0, item.AbsenceRatePercent); } [Fact] public void NavigationCommands_RufenDelegatMitPassendemTabIndexAuf() { var (vm, _, _, _, _, _, _) = BuildScenario(); var indices = new List(); vm.OnNavigateToTab = idx => indices.Add(idx); vm.NavigateToPlanningCommand.Execute(null); vm.NavigateToExamsCommand.Execute(null); vm.NavigateToParticipationCommand.Execute(null); vm.NavigateToDocumentationCommand.Execute(null); Assert.Equal([6, 4, 3, 8], indices); } [Fact] public void FehlendeHausaufgaben_ListetNurSchuelerAusDerLetztenSitzung() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var studentMissing = new Student { FirstName = "Anna", LastName = "Beispiel" }; var studentDone = new Student { FirstName = "Ben", LastName = "Muster" }; var students = new FakeStudents([studentMissing, studentDone]); var sessions = new FakeSessions([]); var entries = new FakeEntries(); var olderSession = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-10) }; sessions.Save(olderSession); entries.Add(new ParticipationEntry { SessionId = olderSession.Id, StudentId = studentDone.Id, Homework = HomeworkStatus.MissingOpen, HomeworkMissing = true, }); var lastSession = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-1) }; sessions.Save(lastSession); entries.Add(new ParticipationEntry { SessionId = lastSession.Id, StudentId = studentMissing.Id, Homework = HomeworkStatus.MissingOverdue, HomeworkMissing = true, }); entries.Add(new ParticipationEntry { SessionId = lastSession.Id, StudentId = studentDone.Id, Homework = HomeworkStatus.Completed, HomeworkMissing = false, }); var vm = NewVm(sessions: sessions, entries: entries, students: students); vm.Initialize(groupId, "Testkurs"); var item = Assert.Single(vm.MissingHomeworkStudents); Assert.Equal(studentMissing.FullName, item.StudentName); Assert.Contains(lastSession.Date.ToString("dd.MM.yyyy"), vm.MissingHomeworkSessionLabel); } [Fact] public void FehlendeHausaufgaben_OhneOffenePostenIstLeer() { var (vm, _, _, _, _, _, _) = BuildScenario(); Assert.False(vm.HasMissingHomework); } [Fact] public void AnstehendeAufgaben_ZeigtNurOffeneAufgabenDieserGruppeSortiertNachFaelligkeit() { var today = DateOnly.FromDateTime(DateTime.Today); var groupId = Guid.NewGuid(); var otherGroupId = Guid.NewGuid(); var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { GroupId = groupId, Title = "Später fällig", DueDate = today.AddDays(10), Status = WorkTaskStatus.Open }); tasks.Add(new WorkTask { GroupId = groupId, Title = "Bald fällig", DueDate = today.AddDays(1), Status = WorkTaskStatus.Open }); tasks.Add(new WorkTask { GroupId = groupId, Title = "Erledigt", DueDate = today, Status = WorkTaskStatus.Done }); tasks.Add(new WorkTask { GroupId = otherGroupId, Title = "Anderer Kurs", DueDate = today, Status = WorkTaskStatus.Open }); var vm = NewVm(tasks: tasks); vm.Initialize(groupId, "Testkurs"); Assert.True(vm.HasGroupTasks); Assert.Equal(["Bald fällig", "Später fällig"], vm.GroupTasks.Select(t => t.Title)); } [Fact] public void AnstehendeAufgaben_OhneAufgabenIstLeer() { var (vm, _, _, _, _, _, _) = BuildScenario(); Assert.False(vm.HasGroupTasks); } [Fact] public void NavigateToWorkload_RuftDelegatAuf() { var (vm, _, _, _, _, _, _) = BuildScenario(); var called = false; vm.OnNavigateToWorkload = () => called = true; vm.NavigateToWorkloadCommand.Execute(null); Assert.True(called); } [Fact] public async Task AddGroupTask_SpeichertErgebnisMitGruppeUndAktualisiertListe() { var tasks = new FakeWorkTasks(); var (vm, _, _, _, _, _, groupId) = BuildScenario(); var vmWithTasks = NewVm(tasks: tasks); vmWithTasks.Initialize(groupId, "Testkurs"); vmWithTasks.OnAddGroupTask = requestedGroupId => Task.FromResult(new WorkTask { Title = "Material einsammeln", GroupId = requestedGroupId }); await vmWithTasks.AddGroupTaskCommand.ExecuteAsync(null); var saved = Assert.Single(tasks.GetByGroup(groupId)); Assert.Equal("Material einsammeln", saved.Title); Assert.True(vmWithTasks.HasGroupTasks); Assert.Equal("Material einsammeln", Assert.Single(vmWithTasks.GroupTasks).Title); } [Fact] public async Task AddGroupTask_OhneDelegatMachtNichts() { var (vm, _, _, _, _, _, _) = BuildScenario(); await vm.AddGroupTaskCommand.ExecuteAsync(null); Assert.False(vm.HasGroupTasks); } }