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 bool? LastAllowSessionTrust { get; private set; }
public Task ConfirmAsync(string title, string message, CancellationToken ct,
[System.Runtime.CompilerServices.CallerMemberName] string operationKey = "", bool allowSessionTrust = true)
{
CallCount++;
LastTitle = title;
LastMessage = message;
LastOperationKey = operationKey;
LastAllowSessionTrust = allowSessionTrust;
return Task.FromResult(Response);
}
}