using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.Services.Mcp; using LehrerApp.Desktop.ViewModels.Settings; using LehrerApp.Sync; using LehrerApp.Sync.Crypto; namespace LehrerApp.Desktop.Tests; // Einfache In-Memory-Fakes der Repository-Schnittstellen, damit ViewModel-Tests ohne echte // LiteDB-Anbindung laufen. Bewusst schlank gehalten: nur was die getesteten ViewModels brauchen. public static class TestSupport { /// Für Tests, die eine echte AiSettingsService-Instanz brauchen (dateibasiert wie /// PeriodScheduleService & Co.) — eigenes Temp-Verzeichnis je Aufruf, damit Tests sich nicht /// gegenseitig über dieselbe ai-settings.json/ai-token.key stören. public static AiSettingsService BuildAiSettingsService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-aisettings-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new AiSettingsService(tempPath); } /// Für Tests, die nur einen validen AiPlanningService zum Durchreichen brauchen (z.B. weil /// SettingsViewModel/PlanningTabViewModel ihn im Konstruktor verlangen), nicht seine eigentliche /// Funktionalität testen — leere Fakes genügen, es wird kein echter HTTP-Aufruf ausgelöst, /// solange AiSettingsService.IsLoggedIn false ist. public static AiPlanningService BuildAiPlanningService() => new( new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static McpSettingsService BuildMcpSettingsService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-mcpsettings-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new McpSettingsService(tempPath); } /// Kein Konstruktorparameter - Pfade werden intern über Environment.SpecialFolder aufgelöst. public static McpClientRegistrationService BuildMcpClientRegistrationService() => new(); /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static WebUntisSettingsService BuildWebUntisSettingsService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-webuntissettings-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new WebUntisSettingsService(tempPath); } /// Nicht konfiguriert (kein API-Login hinterlegt) - genügt für Tests, die nur den Konstruktor /// bedienen müssen und keinen echten WebUntis-Zugriff auslösen. public static WebUntisIntegrationService BuildWebUntisIntegrationService() => new(new HttpClient(), BuildWebUntisSettingsService()); public static UntisHubService BuildUntisHubService(List? groups = null) => new(new FakeGroups(groups ?? []), new FakeUntisHubJobStates(), new SchoolYearService()); /// Analog zu den übrigen dateibasierten Feed-Einstellungen: eigenes Temp-Verzeichnis. public static AnnualPlanSettingsService BuildAnnualPlanSettingsService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-annualplansettings-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new AnnualPlanSettingsService(tempPath); } /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static SyncSettingsService BuildSyncSettingsService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-syncsettings-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new SyncSettingsService(tempPath); } /// Kein echter HTTP-Aufruf, solange SyncSettingsService.IsLoggedIn false ist (siehe /// BuildAiPlanningService). public static SyncAuthService BuildSyncAuthService() => new(new HttpClient()); /// Eigenes Temp-Verzeichnis je Aufruf (echte, dateibasierte LiteDB wie bei EventQueue üblich). public static EventQueue BuildEventQueue() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-eventqueue-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new EventQueue(Path.Combine(tempPath, "queue.db")); } /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static PeriodScheduleService BuildPeriodScheduleService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-periodschedule-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new PeriodScheduleService(tempPath); } /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static AppLogger BuildAppLogger() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-applogger-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new AppLogger(tempPath); } /// Für Tests, die nicht speziell den "Schlüssel neu erzeugt"-Warnzustand prüfen. public static SyncKeyStatus BuildSyncKeyStatus(bool keyWasRegenerated = false) => new(keyWasRegenerated); /// Neue, leere Fakes je Aufruf - für Tests, die nicht speziell TrashViewModel-Verhalten prüfen. public static TrashViewModel BuildTrashViewModel() => new( new FakeTrash(), new FakeGrades(), new FakeWorkTasks(), new FakeTimeEntries(), new FakeSeatingPlans(), new FakeGradingKeyTemplates()); /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static AppearanceSettingsService BuildAppearanceSettingsService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-appearance-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new AppearanceSettingsService(tempPath); } /// Eigenes Temp-Verzeichnis je Aufruf, damit Tests sich nicht gegenseitig über dieselbe /// sync.key stören. public static SyncKeyRecoveryService BuildSyncKeyRecoveryService() { var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-synckeyrecovery-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new SyncKeyRecoveryService(SyncCrypto.GenerateKey(), Path.Combine(tempPath, "sync.key")); } } public class FakeStudents(List all) : IStudentRepository { private readonly Dictionary _references = []; public Student? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id); public List GetAll(bool includeInactive = false) => includeInactive ? all.ToList() : all.Where(s => s.IsActive).ToList(); public List GetByGroup(Guid groupId) => all; public StudentReferenceSummary GetReferenceSummary(Guid studentId) => _references.GetValueOrDefault(studentId, new StudentReferenceSummary(0, 0, 0, 0, 0, 0)); public void SetReferenceSummary(Guid studentId, StudentReferenceSummary summary) => _references[studentId] = summary; public void Save(Student student) { all.RemoveAll(s => s.Id == student.Id); all.Add(student); } public void Delete(Guid id) { if (GetReferenceSummary(id).HasReferences) throw new InvalidOperationException(); all.RemoveAll(s => s.Id == id); } } public class FakeMemberships(List all) : IGroupMembershipRepository { public List GetByStudent(Guid studentId) => all.Where(m => m.StudentId == studentId).ToList(); public List GetByGroup(Guid groupId) => all.Where(m => m.GroupId == groupId).ToList(); public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) => all.FirstOrDefault(m => m.StudentId == studentId && m.GroupId == groupId); public void Save(GroupMembership membership) { all.RemoveAll(m => m.Id == membership.Id); all.Add(membership); } public void Delete(Guid id) => all.RemoveAll(m => m.Id == id); } public class FakeSeatingPlans(List? initial = null) : ISeatingPlanRepository { private readonly List _all = initial ?? []; public SeatingPlan? GetById(Guid id) => _all.FirstOrDefault(p => p.Id == id); public List GetByGroup(Guid groupId) => _all.Where(p => p.GroupId == groupId).OrderBy(p => p.Name).ToList(); public void Save(SeatingPlan plan) { _all.RemoveAll(p => p.Id == plan.Id); _all.Add(plan); } public void Delete(Guid id) => _all.RemoveAll(p => p.Id == id); // Trash-Mechanismus (14.3) lebt real in LiteDbContext.MoveToTrash/RestoreFromTrash - Fakes // bilden ihn bewusst nicht nach, ViewModel-Tests brauchen das nicht. public void Restore(Guid trashId) { } } public class FakeTrash(List? initial = null) : ITrashRepository { private readonly List _all = initial ?? []; public void Add(TrashedItem item) => _all.Add(item); public List GetAll() => _all.OrderByDescending(t => t.DeletedAt).ToList(); public void PurgeOlderThan(DateTime cutoffUtc) => _all.RemoveAll(t => t.DeletedAt < cutoffUtc); } public class FakeSessions(List all) : IParticipationSessionRepository { public List GetByGroup(Guid groupId) => all.Where(s => s.GroupId == groupId).ToList(); public ParticipationSession? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id); public List GetAll() => all.ToList(); public void Save(ParticipationSession session) { all.RemoveAll(s => s.Id == session.Id); all.Add(session); } public void Delete(Guid id) => all.RemoveAll(s => s.Id == id); } public class FakeEntries : IParticipationRepository { private readonly List _all = []; public void Add(ParticipationEntry e) => _all.Add(e); public List GetBySession(Guid sessionId) => _all.Where(e => e.SessionId == sessionId).ToList(); public List GetByStudent(Guid studentId) => _all.Where(e => e.StudentId == studentId).ToList(); public ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId) => _all.FirstOrDefault(e => e.SessionId == sessionId && e.StudentId == studentId); public void Save(ParticipationEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); } public void SaveMany(List entries) { foreach (var e in entries) Save(e); } public void DeleteBySession(Guid sessionId) => _all.RemoveAll(e => e.SessionId == sessionId); } public class FakeAspects : IParticipationAspectRepository { private readonly List _all = []; public List GetDefaults() => _all.Where(a => a.GroupId == null && a.IsActive).OrderBy(a => a.SortOrder).ToList(); public List GetByGroup(Guid groupId) => _all.Where(a => a.GroupId == groupId && a.IsActive).OrderBy(a => a.SortOrder).ToList(); public List GetAllByGroup(Guid groupId) => _all.Where(a => a.GroupId == groupId).OrderBy(a => a.SortOrder).ToList(); public void Save(ParticipationAspect a) { a.Key = a.Key.Trim(); a.Label = a.Label.Trim(); if (a.Key.Length == 0) throw new ArgumentException("Der Schlüssel darf nicht leer sein."); if (a.Label.Length == 0) throw new ArgumentException("Die Bezeichnung darf nicht leer sein."); var duplicate = _all.Where(x => x.GroupId == null || x.GroupId == a.GroupId) .FirstOrDefault(x => x.Id != a.Id && string.Equals(x.Key, a.Key, StringComparison.OrdinalIgnoreCase)); if (duplicate is not null) throw new InvalidOperationException("Ein Aspekt mit diesem Schlüssel existiert für diese Gruppe bereits."); _all.RemoveAll(x => x.Id == a.Id); _all.Add(a); } public void Delete(Guid id) => _all.RemoveAll(a => a.Id == id); } public class FakeGrades : IGradeRepository { private readonly List _all = []; public void Add(Grade g) => _all.Add(g); public List GetByStudentAndGroup(Guid studentId, Guid groupId) => _all.Where(g => g.StudentId == studentId && g.GroupId == groupId).ToList(); public List GetByGroup(Guid groupId) => _all.Where(g => g.GroupId == groupId).ToList(); public void Save(Grade grade) { _all.RemoveAll(g => g.Id == grade.Id); _all.Add(grade); } public void Delete(Guid id) => _all.RemoveAll(g => g.Id == id); public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore } public class FakeExams(List all) : IExamRepository { public Exam? GetById(Guid id) => all.FirstOrDefault(e => e.Id == id); public List GetAll() => all; public List GetByGroup(Guid groupId) => all.Where(e => e.GroupId == groupId).ToList(); public void Save(Exam exam) { } public void Delete(Guid id) { } } public class FakeResults : IExamResultRepository { private readonly List _all = []; // Upsert-Semantik nach ExamId+StudentId, wie der reale unique Index es erzwingt. public void Add(ExamResult r) { _all.RemoveAll(x => x.ExamId == r.ExamId && x.StudentId == r.StudentId); _all.Add(r); } public List GetByExam(Guid examId) => _all.Where(r => r.ExamId == examId).ToList(); public List GetByStudent(Guid studentId) => _all.Where(r => r.StudentId == studentId).ToList(); public ExamResult? GetByExamAndStudent(Guid examId, Guid studentId) => _all.FirstOrDefault(r => r.ExamId == examId && r.StudentId == studentId); public void Save(ExamResult result) { } public void SaveMany(List results) { } } public class FakeGradingKeyTemplates : IGradingKeyTemplateRepository { private readonly List _all = []; public List GetAll() => _all; public List GetByGradingSystem(GradingSystem system) => _all.Where(t => t.GradingSystem == system).ToList(); public GradingKeyTemplate? GetById(Guid id) => _all.FirstOrDefault(t => t.Id == id); public void Save(GradingKeyTemplate template) { _all.RemoveAll(t => t.Id == template.Id); _all.Add(template); } public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id); public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore } public class FakeSchemes : IGradingSchemeRepository { private readonly Dictionary _byGroup = []; private readonly Dictionary _byType = []; public void SetForGroup(Guid groupId, GradingScheme s) => _byGroup[groupId] = s; public void SetDefault(GroupType type, GradingScheme s) => _byType[type] = s; public GradingScheme? GetByGroup(Guid groupId) => _byGroup.GetValueOrDefault(groupId); public GradingScheme? GetDefaultForType(GroupType type) => _byType.GetValueOrDefault(type); public void Save(GradingScheme scheme) { } public void Delete(Guid id) { } } public class FakeGroups(List all) : IGroupRepository { public LearningGroup? GetById(Guid id) => all.FirstOrDefault(g => g.Id == id); public List GetAll(bool includeInactive = false) => all; public List GetBySchoolYear(string schoolYear, bool includeInactive = false) => all; public void Save(LearningGroup group) { } public void Delete(Guid id) { } } public class FakeDocumentation : IDocumentationRepository { private readonly List _all = []; public void Add(Documentation d) => _all.Add(d); public List GetByStudent(Guid studentId) => _all.Where(d => d.StudentId == studentId && !d.IsDeleted).ToList(); public List GetByStudentAndType(Guid studentId, DocumentationType type) => _all.Where(d => d.StudentId == studentId && d.Type == type && !d.IsDeleted).ToList(); public List GetAll() => _all.Where(d => !d.IsDeleted).ToList(); public void Save(Documentation doc) { _all.RemoveAll(d => d.Id == doc.Id); _all.Add(doc); } public void Delete(Guid id) { var d = _all.FirstOrDefault(x => x.Id == id); if (d is not null) d.IsDeleted = true; } public void HardDelete(Guid id) => _all.RemoveAll(d => d.Id == id); } public class FakeAttachmentStorage : IAttachmentStorage { private readonly Dictionary _blobs = []; public string Upload(string fileName, Stream content) { using var ms = new MemoryStream(); content.CopyTo(ms); var id = Guid.NewGuid().ToString("N"); _blobs[id] = ms.ToArray(); return id; } public Stream? OpenRead(string storageId) => _blobs.TryGetValue(storageId, out var b) ? new MemoryStream(b) : null; public void Delete(string storageId) => _blobs.Remove(storageId); } public class FakeUnits : IUnitRepository { private readonly List _all = []; public void Add(Unit u) => _all.Add(u); public Unit? GetById(Guid id) => _all.FirstOrDefault(u => u.Id == id); public List GetByGroup(Guid groupId) => _all.Where(u => u.GroupId == groupId).OrderBy(u => u.StartDate).ToList(); public void Save(Unit unit) { _all.RemoveAll(u => u.Id == unit.Id); _all.Add(unit); } public void Delete(Guid id) => _all.RemoveAll(u => u.Id == id); } public class FakeLessons : ILessonRepository { private readonly List _all = []; public void Add(Lesson l) => _all.Add(l); public Lesson? GetById(Guid id) => _all.FirstOrDefault(l => l.Id == id); public List GetByUnit(Guid unitId) => _all.Where(l => l.UnitId == unitId).OrderBy(l => l.Date).ThenBy(l => l.LessonNumber).ToList(); public List GetByGroupAndDate(Guid groupId, DateOnly date) => _all.Where(l => l.GroupId == groupId && l.Date == date).ToList(); public List GetByGroupAndRange(Guid groupId, DateOnly from, DateOnly to) => _all.Where(l => l.GroupId == groupId && l.Date >= from && l.Date <= to).OrderBy(l => l.Date).ToList(); public void Save(Lesson lesson) { _all.RemoveAll(l => l.Id == lesson.Id); _all.Add(lesson); } public void Delete(Guid id) => _all.RemoveAll(l => l.Id == id); } public class FakeSubjects(List all) : ISubjectRepository { public List GetAll() => all; public Subject? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id); public Subject? GetByName(string name) => all.FirstOrDefault(s => string.Equals(s.Name.Trim(), name.Trim(), StringComparison.OrdinalIgnoreCase)); public void Save(Subject subject) { if (string.IsNullOrWhiteSpace(subject.Name)) throw new ArgumentException("Der Fachname darf nicht leer sein."); var duplicate = GetByName(subject.Name); if (duplicate is not null && duplicate.Id != subject.Id) throw new InvalidOperationException("Ein Fach mit diesem Namen existiert bereits."); all.RemoveAll(s => s.Id == subject.Id); all.Add(subject); } public void Delete(Guid id) => all.RemoveAll(s => s.Id == id); } public class FakeCompetencyDomains : ICompetencyDomainRepository { private readonly List _all = []; public void Add(CompetencyDomain d) => _all.Add(d); public List GetBySubjectAndGrade(Guid subjectId, int gradeLevel) => _all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel) .OrderBy(d => d.SortOrder).ToList(); public List GetBySubject(Guid subjectId) => _all.Where(d => d.SubjectId == subjectId) .OrderBy(d => d.GradeLevel).ThenBy(d => d.SortOrder).ToList(); public CompetencyDomain? GetById(Guid id) => _all.FirstOrDefault(d => d.Id == id); public void Save(CompetencyDomain domain) { _all.RemoveAll(d => d.Id == domain.Id); _all.Add(domain); } public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id); public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) => _all.RemoveAll(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel); public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List domains) { DeleteBySubjectAndGrade(subjectId, gradeLevel); _all.AddRange(domains); } } public class FakeShorthandCodes(List all) : IShorthandCodeRepository { public List GetAll() => all; public void Save(ShorthandCode code) { } public void Delete(Guid id) { } } public class FakeAlternativeLessonPaths(List all) : IAlternativeLessonPathRepository { public List GetAll() => all; public AlternativeLessonPath? GetById(Guid id) => all.FirstOrDefault(p => p.Id == id); public void Save(AlternativeLessonPath path) { all.RemoveAll(p => p.Id == path.Id); all.Add(path); } public void Delete(Guid id) => all.RemoveAll(p => p.Id == id); } public class FakeTimetableSlots : ITimetableSlotRepository { private readonly List _all = []; public void Add(TimetableSlot s) => _all.Add(s); public List GetAll() => _all.ToList(); public List GetByGroup(Guid groupId) => _all.Where(s => s.GroupId == groupId).ToList(); public void Save(TimetableSlot slot) { var occupied = _all.FirstOrDefault(s => s.Weekday == slot.Weekday && s.PeriodNumber == slot.PeriodNumber); if (occupied is not null && occupied.Id != slot.Id) throw new InvalidOperationException("Diese Stunde ist bereits belegt."); _all.RemoveAll(s => s.Id == slot.Id); _all.Add(slot); } public void Delete(Guid id) => _all.RemoveAll(s => s.Id == id); } public class FakeSchoolHolidays : ISchoolHolidayRepository { private readonly List _all = []; public void Add(SchoolHoliday h) => _all.Add(h); public List GetAll() => _all.ToList(); public void Save(SchoolHoliday holiday) { _all.RemoveAll(h => h.Id == holiday.Id); _all.Add(holiday); } public void Delete(Guid id) => _all.RemoveAll(h => h.Id == id); } public class FakeSupervisionDuties : ISupervisionDutyRepository { private readonly List _all = []; public void Add(SupervisionDuty d) => _all.Add(d); public List GetAll() => _all.ToList(); public void Save(SupervisionDuty duty) { var occupied = _all.FirstOrDefault(d => d.Weekday == duty.Weekday && d.AfterPeriod == duty.AfterPeriod); if (occupied is not null && occupied.Id != duty.Id) throw new InvalidOperationException("Für diese Pause ist bereits eine Aufsicht eingetragen."); _all.RemoveAll(d => d.Id == duty.Id); _all.Add(duty); } public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id); } public class FakeSubstitutionEntries : ISubstitutionEntryRepository { private readonly List _all = []; public void Add(SubstitutionEntry e) => _all.Add(e); public List GetAll() => _all.ToList(); public List GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList(); public SubstitutionEntry? GetByExternalId(string externalId) => _all.FirstOrDefault(e => e.ExternalId == externalId); public void Save(SubstitutionEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); } public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id); } public class FakeUntisSnapshots : IUntisSnapshotRepository { private readonly List _all = []; public void Add(UntisSnapshotEntry e) => _all.Add(e); public List GetAll() => _all.ToList(); public void Save(UntisSnapshotEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); } public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id); } public class FakeUntisSlotMappings : IUntisSlotMappingRepository { private readonly List _all = []; public void Add(UntisSlotMapping m) => _all.Add(m); public List GetAll() => _all.ToList(); public void Save(UntisSlotMapping mapping) { _all.RemoveAll(m => m.Id == mapping.Id); _all.Add(mapping); } public void Delete(Guid id) => _all.RemoveAll(m => m.Id == id); } public class FakeAnnualPlanEvents : IAnnualPlanEventRepository { private readonly List _all = []; public void Add(AnnualPlanEvent e) => _all.Add(e); public List GetAll() => _all.ToList(); public List GetByRange(DateOnly from, DateOnly to) => _all.Where(e => e.StartDate <= to && e.EndDate >= from).ToList(); public AnnualPlanEvent? GetByExternalId(string externalId) => _all.FirstOrDefault(e => e.ExternalId == externalId); public void Save(AnnualPlanEvent entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); } public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id); } public class FakeUntisAbsenceCache : IUntisAbsenceCacheRepository { private readonly List _all = []; public List GetByClassAndRange(string className, int startDate, int endDate) => _all.Where(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate).ToList(); public void ReplaceRange(string className, int startDate, int endDate, IEnumerable entries) { _all.RemoveAll(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate); _all.AddRange(entries); } public void InsertRange(IEnumerable entries) => _all.AddRange(entries); } public class FakeUntisClassRegisterCache : IUntisClassRegisterCacheRepository { private readonly List _all = []; public List GetByClassAndRange(string className, int startDate, int endDate) => _all.Where(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate).ToList(); public void ReplaceRange(string className, int startDate, int endDate, IEnumerable entries) { _all.RemoveAll(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate); _all.AddRange(entries); } public void InsertRange(IEnumerable entries) => _all.AddRange(entries); } public class FakeUntisStudentRosterCache : IUntisStudentRosterCacheRepository { private readonly List _all = []; public List GetByClass(string className) => _all.Where(e => e.ClassName == className).ToList(); public void ReplaceAll(string className, IEnumerable entries) { _all.RemoveAll(e => e.ClassName == className); _all.AddRange(entries); } } public class FakeUntisCacheFetchStates : IUntisCacheFetchStateRepository { private readonly List _all = []; public UntisCacheFetchState? Get(string className, UntisCacheKind kind) => _all.FirstOrDefault(s => s.ClassName == className && s.Kind == kind); public void Save(UntisCacheFetchState state) { _all.RemoveAll(s => s.ClassName == state.ClassName && s.Kind == state.Kind); _all.Add(state); } } public class FakeUntisHubJobStates : IUntisHubJobStateRepository { private readonly List _all = []; public UntisHubJobState? Get(UntisHubJobKind kind, Guid? groupId) => _all.FirstOrDefault(s => s.Kind == kind && s.GroupId == groupId); public List GetAll() => _all.ToList(); public void Save(UntisHubJobState state) { _all.RemoveAll(s => s.Kind == state.Kind && s.GroupId == state.GroupId); _all.Add(state); } } public class FakeWorkTasks : IWorkTaskRepository { private readonly List _all = []; public void Add(WorkTask t) => _all.Add(t); public List GetByStatus(WorkTaskStatus status) => _all.Where(t => t.Status == status).ToList(); public List GetAll() => _all.ToList(); public List GetByGroup(Guid groupId) => _all.Where(t => t.GroupId == groupId).ToList(); public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); } public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id); public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore } public class FakeTimeEntries : ITimeEntryRepository { private readonly List _all = []; public void Add(TimeEntry e) => _all.Add(e); public List GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList(); public List GetByDateRange(DateOnly from, DateOnly to) => _all.Where(e => e.Date >= from && e.Date <= to).ToList(); public List GetByTask(Guid taskId) => _all.Where(e => e.TaskId == taskId).ToList(); public void Save(TimeEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); } public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id); public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore } public class FakeReportGrades : IReportGradeRepository { private readonly List _all = []; public int SavedCount { get; private set; } public List GetByGroup(Guid groupId) => _all.Where(r => r.GroupId == groupId).ToList(); public ReportGrade? GetByStudentGroupPeriod(Guid studentId, Guid groupId, string period) => _all.FirstOrDefault(r => r.StudentId == studentId && r.GroupId == groupId && r.Period == period); public void Save(ReportGrade grade) { _all.RemoveAll(r => r.Id == grade.Id); _all.Add(grade); SavedCount++; } public void Delete(Guid id) => _all.RemoveAll(r => r.Id == id); } /// Fake für MCP-Write-Tool-Tests (Phase 2): antwortet ohne echtes UI, konfigurierbar über /// , merkt sich Titel/Nachricht des letzten Aufrufs zur Prüfung, dass der /// Bestätigungstext tatsächlich menschenlesbar ist (kein rohes JSON/GUID-Dump). public class FakeMcpConfirmation : IMcpConfirmationService { public bool Response { get; set; } = true; public string? LastTitle { get; private set; } public string? LastMessage { get; private set; } public int CallCount { get; private set; } public string? LastOperationKey { get; private set; } public Task ConfirmAsync(string title, string message, CancellationToken ct, [System.Runtime.CompilerServices.CallerMemberName] string operationKey = "") { CallCount++; LastTitle = title; LastMessage = message; LastOperationKey = operationKey; return Task.FromResult(Response); } }