import 'package:flutter_test/flutter_test.dart'; import 'package:marianum_mobile/api/mhsl/custom_timetable_event/custom_timetable_event.dart'; import 'package:marianum_mobile/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart'; import 'package:marianum_mobile/api/webuntis/queries/get_holidays/get_holidays_response.dart'; import 'package:marianum_mobile/api/webuntis/queries/get_timetable/get_timetable_response.dart'; import 'package:marianum_mobile/widget_data/widget_data.dart'; import 'package:marianum_mobile/widget_data/widget_data_mapper.dart'; CustomTimetableEvent _event({ required String id, required String title, required DateTime start, required DateTime end, }) => CustomTimetableEvent( id: id, title: title, description: '', startDate: start, endDate: end, color: 'orange', rrule: '', createdAt: start, updatedAt: start, ); GetTimetableResponseObject _lesson({ required int date, required int startTime, required int endTime, String? code, String? subjectName, String? room, int teacherId = 1, String? teacherName, String? teacherOrgname, String? substText, }) => GetTimetableResponseObject( id: 1, date: date, startTime: startTime, endTime: endTime, code: code, substText: substText, kl: const [], te: teacherName != null ? [ GetTimetableResponseObjectTeacher( teacherId, teacherName, teacherName, teacherOrgname == null ? null : 9, teacherOrgname, null, ), ] : const [], su: subjectName != null ? [ GetTimetableResponseObjectSubject( 5, subjectName, subjectName, ), ] : const [], ro: room != null ? [GetTimetableResponseObjectRoom(7, room, room)] : const [], ); void main() { group('resolveDayAnchor', () { test('weekday before cutoff stays today', () { final anchor = WidgetDataMapper.resolveDayAnchor( DateTime(2026, 5, 6, 10), ); expect(anchor, DateTime(2026, 5, 6)); }); test('weekday after cutoff jumps to next school day', () { final anchor = WidgetDataMapper.resolveDayAnchor( DateTime(2026, 5, 6, 19), ); expect(anchor, DateTime(2026, 5, 7)); }); test('Friday after cutoff jumps to Monday', () { final anchor = WidgetDataMapper.resolveDayAnchor( DateTime(2026, 5, 8, 18), ); expect(anchor, DateTime(2026, 5, 11)); }); test('Saturday morning jumps to Monday', () { final anchor = WidgetDataMapper.resolveDayAnchor( DateTime(2026, 5, 9, 10), ); expect(anchor, DateTime(2026, 5, 11)); }); test('Sunday evening jumps to Monday', () { final anchor = WidgetDataMapper.resolveDayAnchor( DateTime(2026, 5, 10, 22), ); expect(anchor, DateTime(2026, 5, 11)); }); }); group('resolveWeekAnchor', () { test('Tuesday returns the Monday of that week', () { final anchor = WidgetDataMapper.resolveWeekAnchor( DateTime(2026, 5, 5, 10), ); expect(anchor, DateTime(2026, 5, 4)); }); test('Sunday returns next Monday', () { final anchor = WidgetDataMapper.resolveWeekAnchor( DateTime(2026, 5, 10, 10), ); expect(anchor, DateTime(2026, 5, 11)); }); }); group('buildDayData', () { final now = DateTime(2026, 5, 6, 10); test('only includes lessons on the anchor day', () { final lessons = [ _lesson(date: 20260506, startTime: 800, endTime: 845, subjectName: 'MA'), _lesson(date: 20260507, startTime: 800, endTime: 845, subjectName: 'EN'), ]; final data = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, ); expect(data.lessons, hasLength(1)); expect(data.lessons.first.subjectShort, 'MA'); }); test('classifies cancelled and irregular lessons', () { final lessons = [ _lesson( date: 20260506, startTime: 800, endTime: 845, subjectName: 'MA', code: 'cancelled', ), _lesson( date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN', code: 'irregular', ), _lesson( date: 20260506, startTime: 1000, endTime: 1045, subjectName: 'BIO', teacherName: 'Müller', teacherOrgname: 'Schmidt', ), ]; final data = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, ); expect( data.lessons.map((l) => l.status).toList(), [ WidgetLessonStatus.cancelled, WidgetLessonStatus.irregular, WidgetLessonStatus.teacherChanged, ], ); }); test('marks day as holiday when in holiday range', () { final holidays = GetHolidaysResponse({ GetHolidaysResponseObject( 1, 'Pfingsten', 'Pfingstferien', 20260506, 20260510, ), }); final data = WidgetDataMapper.buildDayData( now: now, lessons: const [], subjects: null, rooms: null, holidays: holidays, ); expect(data.isHoliday, isTrue); expect(data.holidayName, 'Pfingstferien'); }); test('lessons are sorted by start time', () { final lessons = [ _lesson( date: 20260506, startTime: 1000, endTime: 1045, subjectName: 'BIO', ), _lesson( date: 20260506, startTime: 800, endTime: 845, subjectName: 'MA', ), _lesson( date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN', ), ]; final data = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, ); expect( data.lessons.map((l) => l.subjectShort).toList(), ['MA', 'EN', 'BIO'], ); }); }); group('event collision bumping', () { final now = DateTime(2026, 5, 6, 10); test('long event bumps every regular lesson it covers', () { final lessons = [ _lesson(date: 20260506, startTime: 800, endTime: 845, subjectName: 'MA'), _lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN'), _lesson(date: 20260506, startTime: 1000, endTime: 1045, subjectName: 'BIO'), ]; final events = GetCustomTimetableEventResponse([ _event( id: 'a', title: 'Wandertag', start: DateTime(2026, 5, 6, 8), end: DateTime(2026, 5, 6, 11), ), ]); final data = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, customEvents: events, ); expect(data.lessons, hasLength(3)); for (final l in data.lessons) { expect(l.siblingCount, 1, reason: '${l.subjectShort} should be bumped'); } }); test('event + same-slot duplicate regular: kept lesson shows +2', () { // User scenario: a long custom event covers the slot, and Webuntis // reports two regular lessons starting at the same time (different // class group). The user wants "+2" — one for the hidden event, one // for the parallel regular lesson — not just "+1". final lessons = [ _lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN'), _lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'MA'), ]; final events = GetCustomTimetableEventResponse([ _event( id: 'long', title: 'Wandertag', start: DateTime(2026, 5, 6, 8), end: DateTime(2026, 5, 6, 12), ), ]); final data = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, customEvents: events, ); expect(data.lessons, hasLength(1)); expect(data.lessons.first.siblingCount, 2); }); test('multi-day event splits into one block per calendar day', () { final events = GetCustomTimetableEventResponse([ _event( id: 'multi', title: 'Klassenfahrt', start: DateTime(2026, 5, 4, 8), end: DateTime(2026, 5, 6, 14), ), ]); final data = WidgetDataMapper.buildWeekData( now: DateTime(2026, 5, 5, 10), lessons: const [], subjects: null, rooms: null, holidays: null, customEvents: events, ); expect(data.lessons, hasLength(3)); expect(data.lessons[0].start, DateTime(2026, 5, 4, 8)); expect(data.lessons[0].end, DateTime(2026, 5, 4, 23, 59)); expect(data.lessons[1].start, DateTime(2026, 5, 5, 0)); expect(data.lessons[1].end, DateTime(2026, 5, 5, 23, 59)); expect(data.lessons[2].start, DateTime(2026, 5, 6, 0)); expect(data.lessons[2].end, DateTime(2026, 5, 6, 14)); }); test('two events covering the same regular lesson bump it twice', () { final lessons = [ _lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN'), ]; final events = GetCustomTimetableEventResponse([ _event( id: 'long', title: 'Termin lang', start: DateTime(2026, 5, 6, 8), end: DateTime(2026, 5, 6, 12), ), _event( id: 'short', title: 'Termin kurz', start: DateTime(2026, 5, 6, 9), end: DateTime(2026, 5, 6, 10), ), ]); final data = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, customEvents: events, ); expect(data.lessons, hasLength(1)); expect(data.lessons.first.subjectShort, 'EN'); expect(data.lessons.first.siblingCount, 2); }); }); group('buildWeekData', () { final now = DateTime(2026, 5, 5, 10); // Tuesday test('contains lessons across the school week', () { final lessons = [ _lesson(date: 20260504, startTime: 800, endTime: 845, subjectName: 'MO'), _lesson(date: 20260506, startTime: 800, endTime: 845, subjectName: 'WE'), _lesson(date: 20260508, startTime: 800, endTime: 845, subjectName: 'FR'), _lesson(date: 20260511, startTime: 800, endTime: 845, subjectName: 'NEXT'), ]; final data = WidgetDataMapper.buildWeekData( now: now, lessons: lessons, subjects: null, rooms: null, holidays: null, ); // Anchor is Mon 04.05.; week ends Fri 08.05. exclusive of next Mon expect(data.anchorDate, DateTime(2026, 5, 4)); expect( data.lessons.map((l) => l.subjectShort).toList(), ['MO', 'WE', 'FR'], ); }); }); }