using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels.Groups; using Xunit; namespace LehrerApp.Desktop.Tests; /// Tests für die Zeugnisnotenberechnung (2.4): ReportGradeDialogViewModel kombiniert /// Klausuren, Mitarbeit und sonstige Noten gemäß Gewichtungsschema zu einer Zeugnisnote. public class ReportGradeCalculationTests { private static Guid GroupId { get; } = Guid.NewGuid(); private static Student Anna { get; } = new() { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" }; private static Student Ben { get; } = new() { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" }; private static ReportGradeDialogViewModel BuildViewModel( FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades, FakeExams exams, FakeResults results) { var students = new FakeStudents([Anna, Ben]); var memberships = new FakeMemberships([]); var grading = new GradingService(); return new ReportGradeDialogViewModel(grades, exams, results, students, memberships, schemes, reportGrades, grading, GroupId, GroupType.Class, GradingSystem.Grades1To6, "Testgruppe"); } private static (Exam Exam1, Exam Exam2, FakeExams Exams, FakeResults Results) BuildExams() { var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" }; var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" }; var exams = new FakeExams([exam1, exam2]); var results = new FakeResults(); results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" }); results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "4" }); return (exam1, exam2, exams, results); } [Fact] public void CalculatedGrade_KombiniertKlausurenMitarbeitUndSonstigeGemaessSchema() { var (_, _, exams, results) = BuildExams(); var grades = new FakeGrades(); grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Participation, Date = new DateOnly(2025, 9, 5), Value = "3", Weight = 1.0 }); grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Other, Date = new DateOnly(2025, 9, 5), Value = "1", Weight = 1.0 }); var schemes = new FakeSchemes(); schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 }); var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results); // Klausuren Ø3 (Note 2 und 4), Mitarbeit 3, Sonstige 1 -> (3*50+3*40+1*10)/100 = 2.8 -> kaufmännisch 3 var anna = vm.Rows.Single(r => r.StudentId == Anna.Id); Assert.Equal("3", anna.CalculatedValue); } [Fact] public void CalculatedGrade_SchuelerOhneJedeNote_ZeigtKeinenWert() { var (_, _, exams, results) = BuildExams(); var schemes = new FakeSchemes(); var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results); var ben = vm.Rows.Single(r => r.StudentId == Ben.Id); Assert.Null(ben.CalculatedValue); Assert.Equal("–", ben.CalculatedDisplay); } [Fact] public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung() { var (_, _, exams, results) = BuildExams(); var grades = new FakeGrades(); var schemes = new FakeSchemes(); schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 }); schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 0, ParticipationPercent = 0, OtherPercent = 100 }); var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results); // Gruppenspezifisches Schema gewichtet nur Klausuren (Ø3 aus Note 2 und 4) -> exakt "3" Assert.Contains("Klausuren 100", vm.SchemeSummary); var anna = vm.Rows.Single(r => r.StudentId == Anna.Id); Assert.Equal("3", anna.CalculatedValue); } [Fact] public void ResolveScheme_OhneGruppenschema_VerwendetVoreinstellungDesGruppentyps() { var (_, _, exams, results) = BuildExams(); var schemes = new FakeSchemes(); schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 70, ParticipationPercent = 20, OtherPercent = 10 }); var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results); Assert.Contains("Klausuren 70", vm.SchemeSummary); } [Fact] public void ResolveScheme_OhneJedeVoreinstellung_VerwendetHartcodiertenFallback() { var (_, _, exams, results) = BuildExams(); var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results); Assert.Contains("Klausuren 50", vm.SchemeSummary); Assert.Contains("Mitarbeit 40", vm.SchemeSummary); Assert.Contains("Sonstige 10", vm.SchemeSummary); } [Fact] public void Save_UebersteuernOhneBegruendung_LiefertValidierungsfehlerUndSpeichertNicht() { var (_, _, exams, results) = BuildExams(); var reportGrades = new FakeReportGrades(); var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results); var anna = vm.Rows.Single(r => r.StudentId == Anna.Id); anna.OverrideValue = "1"; anna.SaveCommand.Execute(null); Assert.False(string.IsNullOrEmpty(anna.ValidationMessage)); Assert.Equal(0, reportGrades.SavedCount); } [Fact] public void Save_UebersteuernMitBegruendung_AktualisiertFinalDisplay() { var (_, _, exams, results) = BuildExams(); var reportGrades = new FakeReportGrades(); var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results); var anna = vm.Rows.Single(r => r.StudentId == Anna.Id); anna.OverrideValue = "1"; anna.OverrideReason = "Mündliche Steigerung im letzten Quartal"; anna.SaveCommand.Execute(null); Assert.True(string.IsNullOrEmpty(anna.ValidationMessage)); Assert.Equal("1", anna.FinalDisplay); Assert.Equal(1, reportGrades.SavedCount); } [Fact] public void ToggleLock_FriertDenStandEinUndIgnoriertSpaetereNeuberechnung() { var (exam1, _, exams, results) = BuildExams(); var reportGrades = new FakeReportGrades(); var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results); var anna = vm.Rows.Single(r => r.StudentId == Anna.Id); anna.OverrideValue = "1"; anna.OverrideReason = "Testgrund"; anna.SaveCommand.Execute(null); anna.ToggleLockCommand.Execute(null); // Neue, bessere Klausurnote nachtragen -> würde die Berechnung ändern, wenn ungesperrt. results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "1" }); vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.H1); vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.FullYear); var annaNachSperre = vm.Rows.Single(r => r.StudentId == Anna.Id); Assert.True(annaNachSperre.IsLocked); Assert.Equal("1", annaNachSperre.FinalDisplay); } [Fact] public void ExportCsv_EnthaeltAlleSchuelerDerGruppe() { var (_, _, exams, results) = BuildExams(); var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results); var csv = vm.ExportCsv(); Assert.Contains("Beispiel, Anna", csv); Assert.Contains("Muster, Ben", csv); } [Fact] public void RoundingRule_Paedagogisch_RundetGrenzfallZurBesserenNote() { var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" }; var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" }; var exams = new FakeExams([exam1, exam2]); var results = new FakeResults(); // Ø exakt 2.5 -> kaufmännisch 3, pädagogisch 2 (bessere Note) results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" }); results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "3" }); var schemes = new FakeSchemes(); schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 }); var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results); Assert.Equal("3", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue); vm.RoundingRuleName = "Pädagogisch"; Assert.Equal("2", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue); } }