import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.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/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,
);

McTimetableEntry _lesson({
  required DateTime date,
  required int startHhmm,
  required int endHhmm,
  String status = 'REGULAR',
  String? subjectName,
  String? room,
  String? teacherName,
  String? teacherOrgname,
  String? substText,
}) => McTimetableEntry(
  id: 1,
  date: date,
  startTime: DateTime(1970, 1, 1, startHhmm ~/ 100, startHhmm % 100),
  endTime: DateTime(1970, 1, 1, endHhmm ~/ 100, endHhmm % 100),
  subjects: subjectName != null ? [subjectName] : const [],
  teachers: teacherName != null
      ? [
          McTimetableTeacher(
            shortName: teacherName,
            displayName: teacherName,
            originalShortName: teacherOrgname,
            originalDisplayName: teacherOrgname,
          ),
        ]
      : const [],
  rooms: room != null ? [room] : const [],
  classNames: const [],
  lessonType: 'LESSON',
  status: status,
  substitutionText: substText,
  lessonText: null,
  infoText: null,
);

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: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'MA'),
        _lesson(date: DateTime(2026, 5, 7), startHhmm: 800, endHhmm: 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: DateTime(2026, 5, 6),
          startHhmm: 800,
          endHhmm: 845,
          subjectName: 'MA',
          status: 'CANCELLED',
        ),
        _lesson(
          date: DateTime(2026, 5, 6),
          startHhmm: 900,
          endHhmm: 945,
          subjectName: 'EN',
          status: 'IRREGULAR',
          teacherName: 'Müller',
        ),
        _lesson(
          date: DateTime(2026, 5, 6),
          startHhmm: 1000,
          endHhmm: 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 = TimetableGetHolidaysResponse(
        result: [
          McHoliday(
            shortName: 'Pfingsten',
            longName: 'Pfingstferien',
            startDate: DateTime(2026, 5, 6),
            endDate: DateTime(2026, 5, 10),
          ),
        ],
      );
      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: DateTime(2026, 5, 6),
          startHhmm: 1000,
          endHhmm: 1045,
          subjectName: 'BIO',
        ),
        _lesson(
          date: DateTime(2026, 5, 6),
          startHhmm: 800,
          endHhmm: 845,
          subjectName: 'MA',
        ),
        _lesson(
          date: DateTime(2026, 5, 6),
          startHhmm: 900,
          endHhmm: 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: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'MA'),
        _lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
        _lesson(date: DateTime(2026, 5, 6), startHhmm: 1000, endHhmm: 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', () {
      final lessons = [
        _lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
        _lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 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: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 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('startOfCalendarWeek', () {
    test('Tuesday maps to its own Monday', () {
      expect(
        WidgetDataMapper.startOfCalendarWeek(DateTime(2026, 5, 5, 10)),
        DateTime(2026, 5, 4),
      );
    });

    test('Sunday stays in the current week (no roll-forward)', () {
      expect(
        WidgetDataMapper.startOfCalendarWeek(DateTime(2026, 5, 10, 22)),
        DateTime(2026, 5, 4),
      );
    });
  });

  group('buildWeekData', () {
    final now = DateTime(2026, 5, 5, 10); // Tuesday

    test('covers the 14-day window including next week', () {
      final lessons = [
        _lesson(date: DateTime(2026, 5, 4), startHhmm: 800, endHhmm: 845, subjectName: 'MO'),
        _lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'WE'),
        _lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
        _lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
        _lesson(date: DateTime(2026, 5, 18), startHhmm: 800, endHhmm: 845, subjectName: 'FAR'),
      ];
      final data = WidgetDataMapper.buildWeekData(
        now: now,
        lessons: lessons,
        subjects: null,
        rooms: null,
        holidays: null,
      );
      expect(data.anchorDate, DateTime(2026, 5, 4));
      expect(
        data.lessons.map((l) => l.subjectShort).toList(),
        ['MO', 'WE', 'FR', 'NEXT'],
      );
    });

    test('window stays on the current week when the anchor rolls forward', () {
      // Friday evening: day/week anchors jump to next Monday, but the window
      // still starts at the current week's Monday so today stays available.
      final fridayEvening = DateTime(2026, 5, 8, 18);
      final lessons = [
        _lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
        _lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
      ];
      final data = WidgetDataMapper.buildWeekData(
        now: fridayEvening,
        lessons: lessons,
        subjects: null,
        rooms: null,
        holidays: null,
      );
      expect(data.anchorDate, DateTime(2026, 5, 11));
      expect(
        data.lessons.map((l) => l.subjectShort).toList(),
        ['FR', 'NEXT'],
      );
    });

    test('carries per-day holiday info in days', () {
      final holidays = TimetableGetHolidaysResponse(
        result: [
          McHoliday(
            shortName: 'Pfingsten',
            longName: 'Pfingstferien',
            startDate: DateTime(2026, 5, 14),
            endDate: DateTime(2026, 5, 15),
          ),
        ],
      );
      final data = WidgetDataMapper.buildWeekData(
        now: now,
        lessons: const [],
        subjects: null,
        rooms: null,
        holidays: holidays,
      );
      expect(data.days, hasLength(WidgetDataMapper.weekWindowDays));
      expect(data.days.first.date, DateTime(2026, 5, 4));
      expect(data.days.last.date, DateTime(2026, 5, 17));
      final holidayDays =
          data.days.where((d) => d.isHoliday).map((d) => d.date).toList();
      expect(holidayDays, [DateTime(2026, 5, 14), DateTime(2026, 5, 15)]);
      expect(
        data.days.firstWhere((d) => d.isHoliday).holidayName,
        'Pfingstferien',
      );
    });

    test('sets top-level holiday flags for the anchor day', () {
      final holidays = TimetableGetHolidaysResponse(
        result: [
          McHoliday(
            shortName: 'Oster',
            longName: 'Osterferien',
            startDate: DateTime(2026, 5, 4),
            endDate: DateTime(2026, 5, 8),
          ),
        ],
      );
      final data = WidgetDataMapper.buildWeekData(
        now: now,
        lessons: const [],
        subjects: null,
        rooms: null,
        holidays: holidays,
      );
      expect(data.isHoliday, isTrue);
      expect(data.holidayName, 'Osterferien');
    });
  });
}
