import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Surprise task logging', () { final now = DateTime(2026, 6, 19, 12); const service = SurpriseTaskLogService(); test('creates a completed surprise task that occupies the logged time', () { final result = service.log( request: SurpriseTaskLogRequest( id: 'surprise-1', title: 'Answered urgent text', createdAt: now, startedAt: DateTime(2026, 6, 19, 9), timeUsedMinutes: 20, projectId: 'relationships', priority: PriorityLevel.high, reward: RewardLevel.medium, ), input: emptyInput(), updatedAt: now, ); expect(result.surpriseTask.type, TaskType.surprise); expect(result.surpriseTask.status, TaskStatus.completed); expect(result.surpriseTask.title, 'Answered urgent text'); expect(result.surpriseTask.projectId, 'relationships'); expect(result.surpriseTask.priority, PriorityLevel.high); expect(result.surpriseTask.reward, RewardLevel.medium); expect(result.surpriseTask.durationMinutes, 20); expect(result.surpriseTask.scheduledStart, DateTime(2026, 6, 19, 9)); expect(result.surpriseTask.scheduledEnd, DateTime(2026, 6, 19, 9, 20)); expect( result.schedulingResult.tasks.map((task) => task.id), ['surprise-1'], ); }); test('pushes overlapping flexible tasks using normal push rules', () { final flexible = plannedFlexibleTask( id: 'flexible', title: 'Write notes', createdAt: now, start: DateTime(2026, 6, 19, 9), durationMinutes: 30, ); final result = service.log( request: SurpriseTaskLogRequest( id: 'surprise-1', title: 'Unplanned call', createdAt: now, startedAt: DateTime(2026, 6, 19, 9, 10), timeUsedMinutes: 20, ), input: SchedulingInput( tasks: [flexible], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 11), ), ), updatedAt: now, ); final moved = taskById(result.schedulingResult.tasks, 'flexible'); expect(moved.scheduledStart, DateTime(2026, 6, 19, 9, 30)); expect(moved.scheduledEnd, DateTime(2026, 6, 19, 10)); expect(moved.stats.autoPushedCount, 1); expect(moved.stats.manuallyPushedCount, 0); expect(result.schedulingResult.changes.single.taskId, 'flexible'); expect(result.usedNormalPushRules, isTrue); }); test('does not move inflexible or critical overlaps and reports them', () { final inflexible = fixedTask( id: 'appointment', type: TaskType.inflexible, createdAt: now, start: DateTime(2026, 6, 19, 9), durationMinutes: 30, ); final critical = fixedTask( id: 'meds', type: TaskType.critical, createdAt: now, start: DateTime(2026, 6, 19, 9, 20), durationMinutes: 20, ); final result = service.log( request: SurpriseTaskLogRequest( id: 'surprise-1', title: 'Unplanned call', createdAt: now, startedAt: DateTime(2026, 6, 19, 9, 10), timeUsedMinutes: 20, ), input: SchedulingInput( tasks: [inflexible, critical], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 11), ), ), updatedAt: now, ); expect(taskById(result.schedulingResult.tasks, 'appointment'), sameScheduleAs(inflexible)); expect(taskById(result.schedulingResult.tasks, 'meds'), sameScheduleAs(critical)); expect( result.requiredOverlaps.map((overlap) => overlap.taskId), ['appointment', 'meds'], ); expect( result.schedulingResult.overlaps.map((overlap) => overlap.taskId), ['appointment', 'meds'], ); }); test('tracks locked overlap without rendering locked time as a task', () { final result = service.log( request: SurpriseTaskLogRequest( id: 'surprise-1', title: 'Unplanned call', createdAt: now, startedAt: DateTime(2026, 6, 19, 9, 10), timeUsedMinutes: 20, ), input: SchedulingInput( tasks: const [], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 11), ), lockedIntervals: [ TimeInterval( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 10), label: 'hidden-work', ), ], ), updatedAt: now, ); expect(result.lockedOverlaps.single.label, 'hidden-work'); expect(result.schedulingResult.tasks.map((task) => task.type), [ TaskType.surprise, ]); expect(result.schedulingResult.overlaps, isEmpty); }); test('optional fields can be omitted without blocking save', () { final result = service.log( request: SurpriseTaskLogRequest( id: 'surprise-1', title: ' Took a break ', createdAt: now, ), input: emptyInput(), updatedAt: now, ); expect(result.surpriseTask.title, 'Took a break'); expect(result.surpriseTask.projectId, 'inbox'); expect(result.surpriseTask.priority, isNull); expect(result.surpriseTask.reward, RewardLevel.notSet); expect(result.surpriseTask.durationMinutes, isNull); expect(result.surpriseTask.scheduledStart, isNull); expect(result.surpriseTask.scheduledEnd, isNull); expect(result.schedulingResult.tasks.single.id, 'surprise-1'); }); test('does not push the same flexible task twice after cascade movement', () { final first = plannedFlexibleTask( id: 'first', title: 'First', createdAt: now, start: DateTime(2026, 6, 19, 9), durationMinutes: 30, ); final second = plannedFlexibleTask( id: 'second', title: 'Second', createdAt: now, start: DateTime(2026, 6, 19, 9, 30), durationMinutes: 30, ); final result = service.log( request: SurpriseTaskLogRequest( id: 'surprise-1', title: 'Unplanned call', createdAt: now, startedAt: DateTime(2026, 6, 19, 9, 10), timeUsedMinutes: 40, ), input: SchedulingInput( tasks: [first, second], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 12), ), ), updatedAt: now, ); expect(taskById(result.schedulingResult.tasks, 'first').scheduledStart, DateTime(2026, 6, 19, 9, 50)); expect(taskById(result.schedulingResult.tasks, 'second').scheduledStart, DateTime(2026, 6, 19, 10, 20)); expect( result.schedulingResult.changes.map((change) => change.taskId), ['first', 'second'], ); }); }); } SchedulingInput emptyInput() { return SchedulingInput( tasks: const [], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 17), ), ); } Task plannedFlexibleTask({ required String id, required String title, required DateTime createdAt, required DateTime start, required int durationMinutes, }) { return Task.quickCapture( id: id, title: title, createdAt: createdAt, ).copyWith( status: TaskStatus.planned, durationMinutes: durationMinutes, scheduledStart: start, scheduledEnd: start.add(Duration(minutes: durationMinutes)), ); } Task fixedTask({ required String id, required TaskType type, required DateTime createdAt, required DateTime start, required int durationMinutes, }) { return Task( id: id, title: id, projectId: 'fixed', type: type, status: TaskStatus.planned, priority: PriorityLevel.high, durationMinutes: durationMinutes, scheduledStart: start, scheduledEnd: start.add(Duration(minutes: durationMinutes)), createdAt: createdAt, updatedAt: createdAt, ); } Task taskById(List tasks, String id) { return tasks.singleWhere((task) => task.id == id); } Matcher sameScheduleAs(Task expected) { return predicate((actual) { return actual.scheduledStart == expected.scheduledStart && actual.scheduledEnd == expected.scheduledEnd && actual.status == expected.status && actual.type == expected.type; }, 'same schedule and state as ${expected.id}'); }