using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels.Groups; using Xunit; namespace LehrerApp.Desktop.Tests; /// Tests für die Mitarbeits-Aggregation (3.2): ParticipationGradeDialogViewModel /// verrechnet die Sitzungsbewertungen eines Zeitraums zu einer Mitarbeitsnote je Schüler. public class ParticipationGradeAggregationTests { private static (ParticipationGradeDialogViewModel Vm, Guid AnnaId, Guid BenId, Guid CoraId, Guid GroupId, FakeGrades Grades) BuildScenario() { var groupId = Guid.NewGuid(); var anna = new Student { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" }; var ben = new Student { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" }; var cora = new Student { Id = Guid.NewGuid(), FirstName = "Cora", LastName = "Ohne" }; // nie bewertet var students = new FakeStudents([anna, ben, cora]); var memberships = new FakeMemberships([]); var aspects = new FakeAspects(); var sess1 = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 1) }; var sess2 = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 15) }; var sess3 = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 29) }; var sessions = new FakeSessions([sess1, sess2, sess3]); var entries = new FakeEntries(); // Anna: −1, 0, +2 → steigender Trend, beide Standardaspekte gleich bewertet. entries.Add(new ParticipationEntry { SessionId = sess1.Id, StudentId = anna.Id, Ratings = [new() { Key = "quality", Value = -1 }, new() { Key = "quantity", Value = -1 }], }); entries.Add(new ParticipationEntry { SessionId = sess2.Id, StudentId = anna.Id, Ratings = [new() { Key = "quality", Value = 0 }, new() { Key = "quantity", Value = 0 }], }); entries.Add(new ParticipationEntry { SessionId = sess3.Id, StudentId = anna.Id, Ratings = [new() { Key = "quality", Value = 2 }, new() { Key = "quantity", Value = 2 }], }); // Ben: nur einmal bewertet. entries.Add(new ParticipationEntry { SessionId = sess1.Id, StudentId = ben.Id, Ratings = [new() { Key = "quality", Value = 0 }], }); // Cora: nie bewertet. var grades = new FakeGrades(); var grading = new GradingService(); var vm = new ParticipationGradeDialogViewModel(sessions, entries, aspects, students, memberships, grades, grading, groupId, "2025/26", GradingSystem.Grades1To6); return (vm, anna.Id, ben.Id, cora.Id, groupId, grades); } [Fact] public void Recompute_MittelBerechnungGewichtetAlleSitzungenGleich() { var (vm, annaId, _, _, _, _) = BuildScenario(); var anna = vm.Rows.Single(r => r.StudentId == annaId); Assert.Equal(3, anna.SessionCount); Assert.Equal((-1.0 + 0.0 + 2.0) / 3.0, double.Parse(anna.AverageDisplay.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture), precision: 2); } [Fact] public void Recompute_NichtBewerteterSchueler_HatKeineNoteStattSchlechterNote() { var (vm, _, _, coraId, _, _) = BuildScenario(); var cora = vm.Rows.Single(r => r.StudentId == coraId); Assert.Equal(0, cora.SessionCount); Assert.Null(cora.Grade); Assert.Equal("nicht bewertet", cora.GradeDisplay); } [Fact] public void Recompute_EinzelneBewertung_ErgibtTrotzdemEineNote() { var (vm, _, benId, _, _, _) = BuildScenario(); var ben = vm.Rows.Single(r => r.StudentId == benId); Assert.Equal(1, ben.SessionCount); Assert.NotNull(ben.Grade); } [Fact] public void Recompute_SteigendeBewertungen_ZeigenAufwaertstrend() { var (vm, annaId, _, _, _, _) = BuildScenario(); var anna = vm.Rows.Single(r => r.StudentId == annaId); Assert.Equal("↑", anna.TrendSymbol); } [Fact] public void AspektGewicht_AufNullGesetzt_NimmtAspektAusDerBerechnungHeraus() { var (vm, annaId, _, _, _, _) = BuildScenario(); var quantity = vm.AspectWeights.First(a => a.Key == "quantity"); quantity.Weight = 0; // Nur noch "quality" zählt: (-1 + 0 + 2) / 3 = 0.33 — identisch zum Gesamtmittel, // da beide Aspekte bei Anna durchgehend gleich bewertet wurden. var anna = vm.Rows.Single(r => r.StudentId == annaId); var average = double.Parse(anna.AverageDisplay.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture); Assert.Equal(1.0 / 3.0, average, precision: 2); } [Fact] public void SelectedPeriod_H1_SchliesstSitzungenAusserhalbAus() { var (vm, _, _, _, _, _) = BuildScenario(); vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.H2); // Alle Testsitzungen liegen im September (H1) -> im H2-Zeitraum bleibt niemand bewertet. Assert.All(vm.Rows, r => Assert.Equal(0, r.SessionCount)); } [Fact] public void Apply_SpeichertEineNoteJeBewertetemSchueler() { var (vm, annaId, benId, coraId, groupId, grades) = BuildScenario(); vm.ApplyCommand.Execute(null); Assert.NotNull(grades.GetByStudentAndGroup(annaId, groupId) .SingleOrDefault(g => g.Category == GradeCategory.Participation)); Assert.NotNull(grades.GetByStudentAndGroup(benId, groupId) .SingleOrDefault(g => g.Category == GradeCategory.Participation)); Assert.Empty(grades.GetByStudentAndGroup(coraId, groupId)); } [Fact] public void Apply_ZweitesMal_AktualisiertBestehendeNoteStattZuDuplizieren() { var (vm, annaId, _, _, groupId, grades) = BuildScenario(); vm.ApplyCommand.Execute(null); vm.ApplyCommand.Execute(null); var annaGrades = grades.GetByStudentAndGroup(annaId, groupId) .Where(g => g.Category == GradeCategory.Participation).ToList(); Assert.Single(annaGrades); } /// 3.1.3: ein Punkte-Aspekt darf nicht als roher Zahlenwert in die Mittelwertbildung /// einfließen (z.B. "4 Punkte" wäre auf der -2..+2-Achse ein Extremwert weit über +2) — /// er muss vorher auf dieselbe Achse normiert werden wie Scale5/Scale3/Binary. [Fact] public void Recompute_PunkteAspekt_WirdVorDerMittelungAufMinus2Bis2AchseNormiert() { var groupId = Guid.NewGuid(); var anna = new Student { FirstName = "Anna", LastName = "Beispiel" }; var students = new FakeStudents([anna]); var memberships = new FakeMemberships([]); var aspects = new FakeAspects(); aspects.Save(new ParticipationAspect { GroupId = groupId, Key = "raised_hand", Label = "Meldungen", ValueType = AspectValueType.Points, MaxPoints = 4, }); var session = new ParticipationSession { GroupId = groupId, Date = new DateOnly(2025, 9, 1) }; var sessions = new FakeSessions([session]); var entries = new FakeEntries(); // 2 von 4 Punkten -> Mitte der Skala, normiert genau auf 0. entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = anna.Id, Ratings = [new() { Key = "raised_hand", Value = 2 }], }); var grades = new FakeGrades(); var grading = new GradingService(); var vm = new ParticipationGradeDialogViewModel(sessions, entries, aspects, students, memberships, grades, grading, groupId, "2025/26", GradingSystem.Grades1To6); var row = Assert.Single(vm.Rows); Assert.Equal(0.0, double.Parse(row.AverageDisplay.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture), precision: 2); } }