import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('V1ApplicationManagementUseCases', () { final now = DateTime.utc(2026, 6, 25, 12); test('queries backlog with owner thresholds, filters, and sorting', () async { final fresh = backlogTask( id: 'fresh', createdAt: now.subtract(const Duration(days: 1)), ); final aging = backlogTask( id: 'aging', createdAt: now.subtract(const Duration(days: 5)), pushed: true, ); final stale = backlogTask( id: 'stale', createdAt: now.subtract(const Duration(days: 10)), pushed: true, ); final planned = backlogTask( id: 'planned', createdAt: now.subtract(const Duration(days: 40)), ).copyWith(status: TaskStatus.planned); final store = InMemoryApplicationUnitOfWork( initialTasks: [fresh, aging, stale, planned], initialOwnerSettings: [ OwnerSettings( ownerId: 'owner-1', timeZoneId: 'UTC', backlogStalenessSettings: const BacklogStalenessSettings( greenMaxAge: Duration(days: 2), blueMaxAge: Duration(days: 6), ), ), ], ); final useCases = V1ApplicationManagementUseCases( applicationStore: store, ); final result = await useCases.getBacklog( GetBacklogRequest( context: appContext(operationId: 'read-backlog', now: now), filter: BacklogFilter.pushed, sortKey: BacklogSortKey.age, ), ); final view = result.requireValue; expect(view.items.map((item) => item.task.id), ['stale', 'aging']); expect(view.items.map((item) => item.stalenessMarker), [ BacklogStalenessMarker.purple, BacklogStalenessMarker.blue, ]); expect( view.items.any((item) => item.task.id == planned.id), isFalse, ); }); test('keeps configured project defaults separate from learned suggestions', () async { final project = ProjectProfile( id: 'home', name: 'Home', colorKey: 'home-blue', defaultReward: RewardLevel.low, defaultDifficulty: DifficultyLevel.easy, defaultReminderProfile: ReminderProfile.gentle, defaultDurationMinutes: 20, ); final statistics = ProjectStatistics( projectId: 'home', completedTaskCount: 3, durationMinuteCounts: const {45: 3}, rewardCounts: const {RewardLevel.high: 3}, difficultyCounts: const {DifficultyLevel.hard: 3}, reminderProfileCounts: const {ReminderProfile.persistent: 3}, ); final store = InMemoryApplicationUnitOfWork( initialProjects: [project], initialProjectStatistics: [statistics], ); final result = await V1ApplicationManagementUseCases( applicationStore: store, ).getProjectDefaults( GetProjectDefaultsRequest( context: appContext(operationId: 'read-projects', now: now), ), ); final resolution = result.requireValue.projects.single; expect(resolution.configuredDurationMinutes, 20); expect(resolution.configuredReward, RewardLevel.low); expect(resolution.configuredDifficulty, DifficultyLevel.easy); expect(resolution.configuredReminderProfile, ReminderProfile.gentle); expect(resolution.suggestions.durationMinutes!.value, 45); expect(resolution.suggestions.reward!.value, RewardLevel.high); expect(resolution.suggestions.difficulty!.value, DifficultyLevel.hard); expect( resolution.suggestions.reminderProfile!.value, ReminderProfile.persistent, ); }); test('archives projects without deleting task history', () async { final task = backlogTask(id: 'task-1', projectId: 'home', createdAt: now); final store = InMemoryApplicationUnitOfWork( initialProjects: [ ProjectProfile(id: 'home', name: 'Home', colorKey: 'home-blue'), ], initialTasks: [task], ); final useCases = V1ApplicationManagementUseCases( applicationStore: store, ); final archive = await useCases.archiveProject( context: appContext(operationId: 'archive-project', now: now), projectId: 'home', ); final visibleProjects = await useCases.getProjectDefaults( GetProjectDefaultsRequest( context: appContext(operationId: 'read-active-projects', now: now), ), ); final allProjects = await useCases.getProjectDefaults( GetProjectDefaultsRequest( context: appContext(operationId: 'read-all-projects', now: now), includeArchived: true, ), ); expect(archive.requireValue.isArchived, isTrue); expect(visibleProjects.requireValue.projects, isEmpty); expect(allProjects.requireValue.projects.single.project.id, 'home'); expect(store.currentTasks.single.id, task.id); expect(store.currentTasks.single.projectId, 'home'); }); test('creates date-scoped locked overrides and archives blocks safely', () async { final day = CivilDate(2026, 6, 25); final block = LockedBlock( id: 'work', name: 'Work', startTime: WallTime(hour: 9, minute: 0), endTime: WallTime(hour: 17, minute: 0), recurrence: LockedBlockRecurrence.weekly( weekdays: {LockedWeekday.thursday}, ), createdAt: now, updatedAt: now, ); final store = InMemoryApplicationUnitOfWork( initialLockedBlocks: [block], ); final useCases = V1ApplicationManagementUseCases( applicationStore: store, ); final replacement = await useCases.replaceOneDayLockedOccurrence( context: appContext(operationId: 'replace-work', now: now), lockedBlockId: 'work', date: day, startTime: WallTime(hour: 10, minute: 0), endTime: WallTime(hour: 12, minute: 0), name: 'Short work', ); await useCases.addOneDayLockedOverride( context: appContext(operationId: 'add-appointment', now: now), date: day, name: 'Appointment', startTime: WallTime(hour: 14, minute: 0), endTime: WallTime(hour: 15, minute: 0), ); var expansion = expandLockedBlocksForDay( blocks: store.currentLockedBlocks, overrides: store.currentLockedOverrides, date: day, timeZoneId: 'UTC', timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), ); expect(replacement.requireValue.type, LockedBlockOverrideType.replace); expect(expansion.occurrences.map((occurrence) => occurrence.name), [ 'Short work', 'Appointment', ]); await useCases.archiveLockedBlock( context: appContext(operationId: 'archive-work', now: now), lockedBlockId: 'work', ); expansion = expandLockedBlocksForDay( blocks: store.currentLockedBlocks, overrides: store.currentLockedOverrides, date: day, timeZoneId: 'UTC', timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), ); expect(store.currentLockedBlocks.single.isArchived, isTrue); expect(store.currentLockedOverrides, hasLength(2)); expect(expansion.occurrences.map((occurrence) => occurrence.name), [ 'Appointment', ]); }); test('updates owner settings with typed reinterpretation warnings', () async { final store = InMemoryApplicationUnitOfWork( initialOwnerSettings: [ OwnerSettings( ownerId: 'owner-1', timeZoneId: 'UTC', dayStart: WallTime(hour: 8, minute: 0), dayEnd: WallTime(hour: 20, minute: 0), ), ], ); final result = await V1ApplicationManagementUseCases( applicationStore: store, ).updateOwnerSettings( context: appContext(operationId: 'settings', now: now), update: OwnerSettingsUpdate( timeZoneId: 'America/Los_Angeles', dayStart: WallTime(hour: 7, minute: 0), compactModeEnabled: true, backlogStalenessSettings: const BacklogStalenessSettings( greenMaxAge: Duration(days: 3), blueMaxAge: Duration(days: 8), ), ), ); final update = result.requireValue; expect(update.settings.timeZoneId, 'America/Los_Angeles'); expect(update.settings.dayStart, WallTime(hour: 7, minute: 0)); expect(update.settings.compactModeEnabled, isTrue); expect(update.settings.backlogStalenessSettings.blueMaxAge.inDays, 8); expect(update.warnings, [ OwnerSettingsWarningCode.timeZoneChanged, OwnerSettingsWarningCode.planningWindowChanged, ]); }); test('acknowledges one-time notices and hides them from Today state', () async { final day = CivilDate(2026, 6, 25); final snapshot = SchedulingStateSnapshot( id: 'rollover', capturedAt: now, window: SchedulingWindow( start: DateTime.utc(2026, 6, 25), end: DateTime.utc(2026, 6, 25, 23, 59), ), tasks: const [], notices: [ SchedulingNotice( 'Moved 2 unfinished tasks to today.', type: SchedulingNoticeType.moved, movementCode: SchedulingMovementCode.unfinishedFlexibleTasksRolledOver, parameters: const {'count': 2}, ), ], ); final store = InMemoryApplicationUnitOfWork( initialSchedulingSnapshots: [snapshot], ); final todayQuery = GetTodayStateQuery( applicationStore: store, timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), ); final useCases = V1ApplicationManagementUseCases( applicationStore: store, ); final before = (await todayQuery.execute( GetTodayStateRequest( context: appContext(operationId: 'today-before', now: now), date: day, ), )) .requireValue; final noticeId = before.pendingNotices.single.noticeId; final acknowledged = await useCases.acknowledgeNotice( context: appContext(operationId: 'ack-notice', now: now), noticeId: noticeId, ); final after = (await todayQuery.execute( GetTodayStateRequest( context: appContext(operationId: 'today-after', now: now), date: day, ), )) .requireValue; expect( noticeId, schedulingNoticeId(snapshotId: 'rollover', noticeIndex: 0)); expect(acknowledged.requireValue.alreadyAcknowledged, isFalse); expect(store.currentNoticeAcknowledgements.single.noticeId, noticeId); expect(after.pendingNotices, isEmpty); }); }); } ApplicationOperationContext appContext({ required String operationId, required DateTime now, }) { return ApplicationOperationContext.start( operationId: operationId, ownerTimeZone: OwnerTimeZoneContext(ownerId: 'owner-1', timeZoneId: 'UTC'), clock: FixedClock(now), idGenerator: SequentialIdGenerator(prefix: operationId), ); } Task backlogTask({ required String id, required DateTime createdAt, String projectId = 'inbox', bool pushed = false, }) { return Task( id: id, title: 'Task $id', projectId: projectId, type: TaskType.flexible, status: TaskStatus.backlog, priority: PriorityLevel.medium, durationMinutes: 30, createdAt: createdAt, updatedAt: createdAt, stats: pushed ? const TaskStatistics(manuallyPushedCount: 1) : const TaskStatistics(), ); }