using LehrerApp.Core.Models; using LehrerApp.Desktop.Services.Mcp; using LehrerApp.Desktop.Services.Mcp.Tools; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class McpToolsTests { // ── McpToolScope ───────────────────────────────────────────────────────────────────────── [Fact] public void AllowedReadTools_EnthaeltGenauDieSechsReadTools() { Assert.Equal( new[] { "get_exams", "get_grades", "get_lesson_plans", "get_schedule", "get_students", "get_time_entries" }, McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal)); } [Fact] public void AllowedWriteTools_EnthaeltGenauDieDreiPhase2WriteTools() { Assert.Equal( new[] { "create_grade_entry", "create_time_entry", "update_student_group_assignment" }, McpToolScope.AllowedWriteTools.OrderBy(n => n, StringComparer.Ordinal)); } [Fact] public void AllowedTools_EnthaeltKeineDokumentationstypen() { // Gesprächsnotizen/Vorfälle/Förderpläne dürfen technisch nie über MCP erreichbar sein // (siehe Planungsdokument) - die Namenskonvention "documentation"/"vorgang" darf nie auftauchen. var allNames = McpToolScope.AllowedReadTools.Concat(McpToolScope.AllowedWriteTools); Assert.DoesNotContain(allNames, n => n.Contains("documentation", StringComparison.OrdinalIgnoreCase) || n.Contains("vorgang", StringComparison.OrdinalIgnoreCase) || n.Contains("note", StringComparison.OrdinalIgnoreCase)); } // ── StudentTools ───────────────────────────────────────────────────────────────────────── [Fact] public void GetStudents_OhneFilter_LiefertNurAktiveSchueler() { var active = new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true }; var inactive = new Student { FirstName = "Ida", LastName = "Inaktiv", IsActive = false }; var tool = new StudentTools(new FakeStudents([active, inactive])); var result = tool.GetStudents(); Assert.Single(result); Assert.Equal(active.Id, result[0].Id); } [Fact] public void GetStudents_IncludeInactive_LiefertAuchInaktive() { var active = new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true }; var inactive = new Student { FirstName = "Ida", LastName = "Inaktiv", IsActive = false }; var tool = new StudentTools(new FakeStudents([active, inactive])); var result = tool.GetStudents(includeInactive: true); Assert.Equal(2, result.Count); } // ── ExamTools ──────────────────────────────────────────────────────────────────────────── [Fact] public void GetExams_OhneIncludeResults_LiefertKeineErgebnisse() { var groupId = Guid.NewGuid(); var exam = new Exam { GroupId = groupId, Title = "Klausur 1" }; var results = new FakeResults(); results.Add(new ExamResult { ExamId = exam.Id, StudentId = Guid.NewGuid(), TotalPoints = 10 }); var tool = new ExamTools(new FakeExams([exam]), results); var dto = Assert.Single(tool.GetExams(groupId)); Assert.Null(dto.Results); } [Fact] public void GetExams_MitIncludeResults_LiefertErgebnisseJeSchueler() { var groupId = Guid.NewGuid(); var studentId = Guid.NewGuid(); var exam = new Exam { GroupId = groupId, Title = "Klausur 1" }; var results = new FakeResults(); results.Add(new ExamResult { ExamId = exam.Id, StudentId = studentId, TotalPoints = 12.5, Grade = "2+" }); var tool = new ExamTools(new FakeExams([exam]), results); var dto = Assert.Single(tool.GetExams(groupId, includeResults: true)); var result = Assert.Single(dto.Results!); Assert.Equal(studentId, result.StudentId); Assert.Equal("2+", result.Grade); } // ── GradeTools ─────────────────────────────────────────────────────────────────────────── [Fact] public void GetGrades_MitStudentId_FiltertAufEinenSchueler() { var groupId = Guid.NewGuid(); var studentA = Guid.NewGuid(); var studentB = Guid.NewGuid(); var grades = new FakeGrades(); grades.Add(new Grade { GroupId = groupId, StudentId = studentA, Value = "2" }); grades.Add(new Grade { GroupId = groupId, StudentId = studentB, Value = "3" }); var tool = new GradeTools(grades, new FakeStudents([]), new FakeMcpConfirmation()); var dto = Assert.Single(tool.GetGrades(groupId, studentA)); Assert.Equal(studentA, dto.StudentId); } [Fact] public async Task CreateGradeEntry_NutzerBestaetigt_SpeichertNote() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var grades = new FakeGrades(); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GradeTools(grades, new FakeStudents([student]), confirmation); var groupId = Guid.NewGuid(); var result = await tool.CreateGradeEntry( student.Id, groupId, GradeCategory.Oral, "2+", new DateOnly(2026, 1, 10)); Assert.True(result.Applied); Assert.NotNull(result.Id); Assert.Single(grades.GetByGroup(groupId)); // Bestätigungstext muss für einen Menschen lesbar sein (Name statt bloßer GUID). Assert.Contains("Anna", confirmation.LastMessage); Assert.DoesNotContain(student.Id.ToString(), confirmation.LastMessage); } [Fact] public async Task CreateGradeEntry_NutzerLehntAb_SpeichertNichts() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var grades = new FakeGrades(); var confirmation = new FakeMcpConfirmation { Response = false }; var tool = new GradeTools(grades, new FakeStudents([student]), confirmation); var groupId = Guid.NewGuid(); var result = await tool.CreateGradeEntry( student.Id, groupId, GradeCategory.Oral, "2+", new DateOnly(2026, 1, 10)); Assert.False(result.Applied); Assert.Empty(grades.GetByGroup(groupId)); Assert.Equal(1, confirmation.CallCount); } // ── ScheduleTools ──────────────────────────────────────────────────────────────────────── [Fact] public void GetSchedule_MitGroupId_FiltertNachGruppe() { var groupId = Guid.NewGuid(); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = groupId, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); slots.Add(new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 2 }); var tool = new ScheduleTools(slots); var dto = Assert.Single(tool.GetSchedule(groupId)); Assert.Equal(groupId, dto.GroupId); } // ── TimeEntryTools ─────────────────────────────────────────────────────────────────────── [Fact] public void GetTimeEntries_FiltertAufDenAngegebenenZeitraum() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Date = new DateOnly(2026, 1, 5), DurationMinutes = 30 }); entries.Add(new TimeEntry { Date = new DateOnly(2026, 2, 1), DurationMinutes = 45 }); var tool = new TimeEntryTools(entries, new FakeGroups([]), new FakeMcpConfirmation()); var dto = Assert.Single(tool.GetTimeEntries(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))); Assert.Equal(30, dto.DurationMinutes); } [Fact] public async Task CreateTimeEntry_NutzerBestaetigt_SpeichertEintrag() { var entries = new FakeTimeEntries(); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new TimeEntryTools(entries, new FakeGroups([]), confirmation); var result = await tool.CreateTimeEntry("Korrektur", new DateOnly(2026, 1, 10), 30); Assert.True(result.Applied); Assert.Single(entries.GetByDateRange(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))); } [Fact] public async Task CreateTimeEntry_NutzerLehntAb_SpeichertNichts() { var entries = new FakeTimeEntries(); var confirmation = new FakeMcpConfirmation { Response = false }; var tool = new TimeEntryTools(entries, new FakeGroups([]), confirmation); var result = await tool.CreateTimeEntry("Korrektur", new DateOnly(2026, 1, 10), 30); Assert.False(result.Applied); Assert.Empty(entries.GetByDateRange(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))); } // ── LessonPlanTools ────────────────────────────────────────────────────────────────────── [Fact] public void GetLessonPlans_LiefertEinheitenDerGruppeUndStundenImZeitraum() { var groupId = Guid.NewGuid(); var units = new FakeUnits(); units.Add(new Unit { GroupId = groupId, Title = "Optik" }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = groupId, Date = new DateOnly(2026, 1, 5), Topic = "Brechung" }); lessons.Add(new Lesson { GroupId = groupId, Date = new DateOnly(2026, 3, 1), Topic = "Später" }); var tool = new LessonPlanTools(units, lessons); var result = tool.GetLessonPlans(groupId, new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31)); Assert.Single(result.Units); var lesson = Assert.Single(result.Lessons); Assert.Equal("Brechung", lesson.Topic); } // ── GroupMembershipTools ───────────────────────────────────────────────────────────────── [Fact] public async Task UpdateStudentGroupAssignment_KeineBestehendeMitgliedschaft_LegtNeueAn() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var group = new LearningGroup { Name = "7a" }; var memberships = new FakeMemberships([]); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GroupMembershipTools(memberships, new FakeStudents([student]), new FakeGroups([group]), confirmation); var result = await tool.UpdateStudentGroupAssignment(student.Id, group.Id, niveau: Niveau.E); Assert.True(result.Applied); var membership = Assert.Single(memberships.GetByStudent(student.Id)); Assert.Equal(Niveau.E, membership.Niveau); Assert.Contains("Anna", confirmation.LastMessage); } [Fact] public async Task UpdateStudentGroupAssignment_BestehendeMitgliedschaftUnveraendert_FragtNichtNochmalNach() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var group = new LearningGroup { Name = "7a" }; var existing = new GroupMembership { StudentId = student.Id, GroupId = group.Id, Niveau = Niveau.G }; var memberships = new FakeMemberships([existing]); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GroupMembershipTools(memberships, new FakeStudents([student]), new FakeGroups([group]), confirmation); var result = await tool.UpdateStudentGroupAssignment(student.Id, group.Id, niveau: Niveau.G); Assert.True(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task UpdateStudentGroupAssignment_UnbekannterSchueler_LiefertFehlerOhneNachfrage() { var group = new LearningGroup { Name = "7a" }; var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GroupMembershipTools(new FakeMemberships([]), new FakeStudents([]), new FakeGroups([group]), confirmation); var result = await tool.UpdateStudentGroupAssignment(Guid.NewGuid(), group.Id); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } }