import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Timeline item view model', () { final createdAt = DateTime(2026, 6, 20, 8); final start = DateTime(2026, 6, 20, 9); final end = DateTime(2026, 6, 20, 9, 30); const project = ProjectProfile( id: 'home', name: 'Home', colorKey: 'project-home', ); final mapper = TimelineItemMapper.fromProjects(const [project]); test('maps task fields to display tokens', () { final task = scheduledTask( id: 'dishes', title: 'Wash dishes', projectId: project.id, type: TaskType.flexible, reward: RewardLevel.high, difficulty: DifficultyLevel.hard, createdAt: createdAt, start: start, end: end, ); final item = mapper.fromTask(task); expect(item.id, 'dishes'); expect(item.displayTitle, 'Wash dishes'); expect(item.taskType, TaskType.flexible); expect(item.projectColorToken, 'project-home'); expect(item.backgroundToken, TimelineBackgroundToken.flexible); expect(item.rewardIconToken, TimelineRewardIconToken.high); expect(item.difficultyIconToken, TimelineDifficultyIconToken.hard); expect(item.category, TimelineItemCategory.taskCard); expect(item.quickActions, [ TimelineQuickAction.done, TimelineQuickAction.push, TimelineQuickAction.backlog, TimelineQuickAction.breakUp, ]); }); test('flexible task cards display duration while retaining position', () { final task = scheduledTask( id: 'laundry', title: 'Fold laundry', projectId: project.id, type: TaskType.flexible, durationMinutes: 25, createdAt: createdAt, start: start, end: start.add(const Duration(minutes: 25)), ); final item = mapper.fromTask(task); expect(item.showsExplicitTime, isFalse); expect(item.durationMinutes, 25); expect(item.start, start); expect(item.end, start.add(const Duration(minutes: 25))); }); test('flexible duration can fall back to scheduled interval', () { final task = scheduledTask( id: 'admin', title: 'Handle admin', projectId: project.id, type: TaskType.flexible, createdAt: createdAt, start: start, end: start.add(const Duration(minutes: 45)), ); final item = mapper.fromTask(task); expect(item.showsExplicitTime, isFalse); expect(item.durationMinutes, 45); }); test('inflexible critical and locked items show explicit times', () { for (final type in [ TaskType.inflexible, TaskType.critical, TaskType.locked, ]) { final task = scheduledTask( id: type.name, title: type.name, projectId: project.id, type: type, createdAt: createdAt, start: start, end: end, ); final item = mapper.fromTask(task); expect(item.showsExplicitTime, isTrue, reason: type.name); expect(item.start, start, reason: type.name); expect(item.end, end, reason: type.name); expect(item.durationMinutes, isNull, reason: type.name); } }); test('locked item is an overlay without normal task quick actions', () { final task = scheduledTask( id: 'sleep', title: 'Sleep', projectId: 'locked', type: TaskType.locked, createdAt: createdAt, start: DateTime(2026, 6, 20, 0), end: DateTime(2026, 6, 20, 7), ); final item = mapper.fromTask(task); expect(item.category, TimelineItemCategory.overlay); expect(item.projectColorToken, 'locked'); expect(item.backgroundToken, TimelineBackgroundToken.locked); expect(item.quickActions, isEmpty); }); test('hidden locked occurrences stay hidden until reveal mode is active', () { final occurrence = LockedBlockOccurrence( name: 'Work', interval: TimeInterval( start: DateTime(2026, 6, 20, 9), end: DateTime(2026, 6, 20, 17), ), hiddenByDefault: true, lockedBlockId: 'work-hours', projectId: project.id, ); final hiddenItem = mapper.fromLockedOccurrence( occurrence, revealHiddenLockedBlocks: false, ); final revealedItem = mapper.fromLockedOccurrence( occurrence, revealHiddenLockedBlocks: true, ); expect(hiddenItem, isNull); expect(revealedItem, isNotNull); expect(revealedItem!.id, 'locked-block:work-hours'); expect(revealedItem.displayTitle, 'Work'); expect(revealedItem.start, DateTime(2026, 6, 20, 9)); expect(revealedItem.end, DateTime(2026, 6, 20, 17)); expect(revealedItem.showsExplicitTime, isTrue); expect(revealedItem.category, TimelineItemCategory.overlay); expect(revealedItem.projectColorToken, 'project-home'); expect(revealedItem.quickActions, isEmpty); }); test('visible locked occurrences can produce overlays without reveal mode', () { final occurrence = LockedBlockOccurrence( name: 'Appointment', interval: TimeInterval( start: DateTime(2026, 6, 20, 11), end: DateTime(2026, 6, 20, 12), ), hiddenByDefault: false, overrideId: 'appointment-override', ); final item = mapper.fromLockedOccurrence( occurrence, revealHiddenLockedBlocks: false, ); expect(item, isNotNull); expect(item!.id, 'locked-override:appointment-override'); expect(item.displayTitle, 'Appointment'); expect(item.category, TimelineItemCategory.overlay); expect(item.taskType, TaskType.locked); expect(item.backgroundToken, TimelineBackgroundToken.locked); expect(item.rewardIconToken, TimelineRewardIconToken.notSet); expect(item.difficultyIconToken, TimelineDifficultyIconToken.notSet); expect(item.showsExplicitTime, isTrue); expect(item.quickActions, isEmpty); }); test('manual compact mode flag controls compact output selection', () { final current = scheduledTask( id: 'current', title: 'Current task', projectId: project.id, type: TaskType.flexible, createdAt: createdAt, start: DateTime(2026, 6, 20, 9), end: DateTime(2026, 6, 20, 10), ); final nextRequired = scheduledTask( id: 'next-required', title: 'Next appointment', projectId: project.id, type: TaskType.inflexible, createdAt: createdAt, start: DateTime(2026, 6, 20, 11), end: DateTime(2026, 6, 20, 11, 30), ); final disabled = mapper.compactStateForTasks( tasks: [current, nextRequired], now: DateTime(2026, 6, 20, 9, 15), manualCompactModeEnabled: false, ); final enabled = mapper.compactStateForTasks( tasks: [current, nextRequired], now: DateTime(2026, 6, 20, 9, 15), manualCompactModeEnabled: true, ); expect(disabled.manualCompactModeEnabled, isFalse); expect(disabled.compactItems, isEmpty); expect(disabled.fullTimelineVisible, isTrue); expect(enabled.manualCompactModeEnabled, isTrue); expect(enabled.currentItem!.id, 'current'); expect(enabled.nextRequiredItem!.id, 'next-required'); expect(enabled.compactItems.map((item) => item.id), [ 'current', 'next-required', ]); expect(enabled.fullTimelineVisible, isFalse); }); test('compact mode can include the next flexible task when requested', () { final required = scheduledTask( id: 'required', title: 'Required', projectId: project.id, type: TaskType.critical, createdAt: createdAt, start: DateTime(2026, 6, 20, 10), end: DateTime(2026, 6, 20, 10, 30), ); final flexible = scheduledTask( id: 'flexible', title: 'Flexible', projectId: project.id, type: TaskType.flexible, createdAt: createdAt, start: DateTime(2026, 6, 20, 11), end: DateTime(2026, 6, 20, 11, 30), ); final state = mapper.compactStateForTasks( tasks: [flexible, required], now: DateTime(2026, 6, 20, 9), manualCompactModeEnabled: true, includeNextFlexibleTask: true, ); expect(state.currentItem, isNull); expect(state.nextRequiredItem!.id, 'required'); expect(state.nextFlexibleItem!.id, 'flexible'); expect(state.compactItems.map((item) => item.id), [ 'required', 'flexible', ]); }); test('compact mode does not activate after pushed or missed tasks', () { final pushed = scheduledTask( id: 'pushed', title: 'Pushed task', projectId: project.id, type: TaskType.flexible, createdAt: createdAt, start: DateTime(2026, 6, 20, 14), end: DateTime(2026, 6, 20, 14, 30), ); final missed = scheduledTask( id: 'missed', title: 'Missed appointment', projectId: project.id, type: TaskType.inflexible, createdAt: createdAt, start: DateTime(2026, 6, 20, 8), end: DateTime(2026, 6, 20, 8, 30), ).copyWith(status: TaskStatus.missed); final state = mapper.compactStateForTasks( tasks: [pushed, missed], now: DateTime(2026, 6, 20, 9), manualCompactModeEnabled: false, ); expect(state.manualCompactModeEnabled, isFalse); expect(state.compactItems, isEmpty); expect(state.fullTimelineVisible, isTrue); }); test('compact mode ignores locked items unless overlay reveal is separate', () { final locked = scheduledTask( id: 'locked', title: 'Hidden blocked time', projectId: 'locked', type: TaskType.locked, createdAt: createdAt, start: DateTime(2026, 6, 20, 9), end: DateTime(2026, 6, 20, 10), ); final required = scheduledTask( id: 'required', title: 'Visible required task', projectId: project.id, type: TaskType.critical, createdAt: createdAt, start: DateTime(2026, 6, 20, 10), end: DateTime(2026, 6, 20, 10, 30), ); final state = mapper.compactStateForTasks( tasks: [locked, required], now: DateTime(2026, 6, 20, 9, 15), manualCompactModeEnabled: true, ); expect(state.currentItem, isNull); expect(state.nextRequiredItem!.id, 'required'); expect(state.compactItems.map((item) => item.id), ['required']); }); }); } Task scheduledTask({ required String id, required String title, required String projectId, required TaskType type, required DateTime createdAt, required DateTime start, required DateTime end, RewardLevel reward = RewardLevel.notSet, DifficultyLevel difficulty = DifficultyLevel.notSet, int? durationMinutes, }) { return Task( id: id, title: title, projectId: projectId, type: type, status: TaskStatus.planned, reward: reward, difficulty: difficulty, durationMinutes: durationMinutes, scheduledStart: start, scheduledEnd: end, createdAt: createdAt, updatedAt: createdAt, ); }