using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Data; using LehrerApp.Desktop.ViewModels.Settings; using LehrerApp.Sync; using LehrerApp.Sync.Models; using System.Linq; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class SettingsViewModelTests { private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null, FakeSupervisionDuties? supervisionDuties = null, FakeSubjects? subjects = null, FakeCompetencyDomains? competencyDomains = null, EventQueue? eventQueue = null) { // Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState, // das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben. var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new SettingsViewModel( subjects ?? new FakeSubjects([]), competencyDomains ?? new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), eventQueue ?? TestSupport.BuildEventQueue()); } [Fact] public void SyncConflicts_ZeigtUnreviewedKonflikteBeimLaden() { var queue = TestSupport.BuildEventQueue(); var conflict = new ConflictEntry { LocalEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" }, RemoteEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" }, Resolution = "RemoteWon", }; queue.AddConflict(conflict); var vm = BuildViewModel(eventQueue: queue); var item = Assert.Single(vm.SyncConflicts); Assert.Equal(conflict.Id, item.Id); Assert.Contains("anderen Gerät", item.ResolutionDisplay); } [Fact] public void MarkConflictReviewed_EntferntKonfliktAusListeUndAusDerQueue() { var queue = TestSupport.BuildEventQueue(); var conflict = new ConflictEntry { LocalEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" }, RemoteEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" }, Resolution = "LocalWon", }; queue.AddConflict(conflict); var vm = BuildViewModel(eventQueue: queue); var item = vm.SyncConflicts.Single(); vm.MarkConflictReviewedCommand.Execute(item); Assert.Empty(vm.SyncConflicts); Assert.Empty(queue.GetUnreviewed()); } [Fact] public void AddSchoolHoliday_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt() { var holidays = new FakeSchoolHolidays(); var vm = BuildViewModel(holidays); vm.NewHolidayName = "Sommerferien"; vm.NewHolidayStartText = "01.07.2026"; vm.NewHolidayEndText = "10.08.2026"; vm.AddSchoolHolidayCommand.Execute(null); Assert.Single(holidays.GetAll()); Assert.Single(vm.SchoolHolidayEntries); } [Fact] public void AddSchoolHoliday_EndeVorBeginn_SetztFehlerUndSpeichertNicht() { var holidays = new FakeSchoolHolidays(); var vm = BuildViewModel(holidays); vm.NewHolidayName = "Ungültig"; vm.NewHolidayStartText = "10.08.2026"; vm.NewHolidayEndText = "01.07.2026"; vm.AddSchoolHolidayCommand.Execute(null); Assert.Empty(holidays.GetAll()); Assert.NotEqual("", vm.HolidayDateError); } [Fact] public void AddSchoolHoliday_FehlenderName_SetztFehlerUndSpeichertNicht() { var holidays = new FakeSchoolHolidays(); var vm = BuildViewModel(holidays); vm.NewHolidayStartText = "01.07.2026"; vm.NewHolidayEndText = "10.08.2026"; vm.AddSchoolHolidayCommand.Execute(null); Assert.Empty(holidays.GetAll()); Assert.NotEqual("", vm.HolidayNameError); } [Fact] public void RemoveSchoolHoliday_EntferntEintragAusRepositoryUndListe() { var holidays = new FakeSchoolHolidays(); var holiday = new SchoolHoliday { Name = "Herbstferien", StartDate = new DateOnly(2026, 10, 12), EndDate = new DateOnly(2026, 10, 24) }; holidays.Add(holiday); var vm = BuildViewModel(holidays); vm.RemoveSchoolHolidayCommand.Execute(vm.SchoolHolidayEntries[0]); Assert.Empty(holidays.GetAll()); Assert.Empty(vm.SchoolHolidayEntries); } [Fact] public void SelectedStateName_Aendern_PersistiertUeberSchoolCalendarSettings() { var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-state-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); var calendarSettings = new SchoolCalendarSettingsService(tempPath); var vm = new SettingsViewModel( new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath), new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue()); vm.SelectedStateName = "Bayern"; Assert.Equal(GermanState.BY, calendarSettings.State); } [Fact] public void SavePeriodTimes_GueltigeEingabe_WirdPersistiert() { var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-periods-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); var periodSchedule = new PeriodScheduleService(tempPath); var vm = new SettingsViewModel( new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule, new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue()); vm.PeriodTimes[0].StartText = "08:00"; vm.PeriodTimes[0].EndText = "08:45"; vm.SavePeriodTimesCommand.Execute(null); Assert.Equal("", vm.PeriodTimesError); Assert.Equal(45, periodSchedule.GetDurationMinutes(1)); } [Fact] public void SavePeriodTimes_EndeVorBeginn_SetztFehlerUndSpeichertNicht() { var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-periods-invalid-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); var periodSchedule = new PeriodScheduleService(tempPath); var vm = new SettingsViewModel( new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath), new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]), new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule, new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(), TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue()); vm.PeriodTimes[0].StartText = "08:45"; vm.PeriodTimes[0].EndText = "08:00"; vm.SavePeriodTimesCommand.Execute(null); Assert.NotEqual("", vm.PeriodTimesError); Assert.Equal(0, periodSchedule.GetDurationMinutes(1)); } [Fact] public void AddSupervisionDuty_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt() { var duties = new FakeSupervisionDuties(); var vm = BuildViewModel(supervisionDuties: duties); vm.NewDutyWeekdayName = "Montag"; vm.NewDutyAfterPeriod = 2; vm.NewDutyLocation = "Pausenhof"; vm.AddSupervisionDutyCommand.Execute(null); Assert.Single(duties.GetAll()); Assert.Single(vm.SupervisionDuties); Assert.Equal(DayOfWeek.Monday, duties.GetAll()[0].Weekday); Assert.Equal(2, duties.GetAll()[0].AfterPeriod); } [Fact] public void AddSupervisionDuty_FehlenderOrt_SetztFehlerUndSpeichertNicht() { var duties = new FakeSupervisionDuties(); var vm = BuildViewModel(supervisionDuties: duties); vm.NewDutyWeekdayName = "Montag"; vm.NewDutyAfterPeriod = 2; vm.AddSupervisionDutyCommand.Execute(null); Assert.Empty(duties.GetAll()); Assert.NotEqual("", vm.NewDutyError); } [Fact] public void AddSupervisionDuty_PauseBereitsBelegt_ZeigtFreundlicheFehlermeldung() { var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(supervisionDuties: duties); vm.NewDutyWeekdayName = "Montag"; vm.NewDutyAfterPeriod = 2; vm.NewDutyLocation = "Bibliothek"; vm.AddSupervisionDutyCommand.Execute(null); Assert.Single(duties.GetAll()); Assert.Equal("Für diese Pause ist bereits eine Aufsicht eingetragen.", vm.NewDutyError); } [Fact] public void RemoveSupervisionDuty_EntferntEintragAusRepositoryUndListe() { var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(supervisionDuties: duties); vm.RemoveSupervisionDutyCommand.Execute(vm.SupervisionDuties[0]); Assert.Empty(duties.GetAll()); Assert.Empty(vm.SupervisionDuties); } [Fact] public void Kompetenzbereiche_Umsortieren_PersistiertSortOrder() { var subject = new Subject { Name = "Biologie", ShortName = "Bio" }; var repository = new FakeCompetencyDomains(); repository.Add(new CompetencyDomain { SubjectId = subject.Id, GradeLevel = 10, Name = "Erster Bereich", SortOrder = 0, }); repository.Add(new CompetencyDomain { SubjectId = subject.Id, GradeLevel = 10, Name = "Zweiter Bereich", SortOrder = 1, }); var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository); vm.CatalogSubject = vm.Subjects.Single(); vm.Domains[1].MoveUpCommand.Execute(null); Assert.Equal(["Zweiter Bereich", "Erster Bereich"], vm.Domains.Select(x => x.Name)); var stored = repository.GetBySubjectAndGrade(subject.Id, 10); Assert.Equal(["Zweiter Bereich", "Erster Bereich"], stored.Select(x => x.Name)); Assert.Equal([0, 1], stored.Select(x => x.SortOrder)); } [Fact] public void Kompetenzen_Umsortieren_PersistiertSortOrder() { var repository = new FakeCompetencyDomains(); var domain = new CompetencyDomain { Name = "Bereich", Items = [ new CompetencyItem { Code = "A", Description = "Erste", SortOrder = 0 }, new CompetencyItem { Code = "B", Description = "Zweite", SortOrder = 1 }, ], }; repository.Add(domain); var vm = new DomainEditItem(domain, repository); vm.Items[1].MoveUpCommand.Execute(null); Assert.Equal(["B", "A"], vm.Items.Select(x => x.Code)); var stored = repository.GetById(domain.Id)!; Assert.Equal(["B", "A"], stored.Items.Select(x => x.Code)); Assert.Equal([0, 1], stored.Items.Select(x => x.SortOrder)); } [Fact] public void KatalogKopieren_BereitetSichereVorschauFuerZielstufeVor() { var subject = new Subject { Name = "Biologie", ShortName = "Bio" }; var repository = new FakeCompetencyDomains(); repository.Add(new CompetencyDomain { SubjectId = subject.Id, GradeLevel = 10, Name = "Erkenntnisgewinnung", Code = "EG", Items = [new CompetencyItem { Code = "EG1", Description = "Modelle verwenden" }], }); var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository); vm.CatalogSubject = vm.Subjects.Single(); var preview = vm.PrepareCatalogCopy(11); Assert.NotNull(preview); Assert.Equal(11, preview.GradeLevel); Assert.Equal(1, preview.NewDomains); Assert.Equal(1, preview.NewCompetencies); vm.ApplyCatalogImport(preview, CompetencyCatalogImportMode.Merge, new HashSet()); var copied = repository.GetBySubjectAndGrade(subject.Id, 11); Assert.Single(copied); Assert.NotEqual(repository.GetBySubjectAndGrade(subject.Id, 10)[0].Id, copied[0].Id); Assert.Equal("EG1", copied[0].Items.Single().Code); } }