import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Project statistics aggregation', () { final createdAt = DateTime.utc(2026, 6, 25, 8); const transitionService = TaskTransitionService(); const aggregationService = ProjectStatisticsAggregationService(); test('applies completed task activities exactly once', () { final task = scheduledTask( id: 'task-1', projectId: 'home', createdAt: createdAt, start: DateTime.utc(2026, 6, 25, 9), reward: RewardLevel.high, difficulty: DifficultyLevel.hard, reminderOverride: ReminderProfile.strict, ).copyWith( stats: const TaskStatistics( manuallyPushedCount: 1, autoPushedCount: 2, ), ); final transition = transitionService.apply( task: task, transitionCode: TaskTransitionCode.complete, operationId: 'complete-task-1', activityId: 'activity-1', occurredAt: DateTime.utc(2026, 6, 25, 10), actualStart: DateTime.utc(2026, 6, 25, 9, 5), actualEnd: DateTime.utc(2026, 6, 25, 9, 25), ); final first = aggregationService.apply( statistics: ProjectStatistics(projectId: 'home'), activities: transition.activities, tasks: [transition.task], ); final retry = aggregationService.apply( statistics: first.statistics, activities: transition.activities, tasks: [transition.task], alreadyAppliedActivityIds: first.appliedActivityIds, ); expect(first.appliedActivityIds, ['activity-1']); expect(first.statistics.completedTaskCount, 1); expect(first.statistics.durationMinuteCounts, {20: 1}); expect(first.statistics.completionTimeBucketCounts, { ProjectCompletionTimeBucket.morning: 1, }); expect(first.statistics.totalPushesBeforeCompletion, 3); expect(first.statistics.completedAfterPushCount, 1); expect(first.statistics.rewardCounts, {RewardLevel.high: 1}); expect(first.statistics.difficultyCounts, {DifficultyLevel.hard: 1}); expect(first.statistics.reminderProfileCounts, { ReminderProfile.strict: 1, }); expect(first.statistics.averagePushesBeforeCompletionNumerator, 3); expect(first.statistics.averagePushesBeforeCompletionDenominator, 1); expect(first.statistics.averagePushesBeforeCompletion, 3); expect(retry.appliedActivityIds, isEmpty); expect(retry.statistics.completedTaskCount, 1); expect(retry.statistics.totalPushesBeforeCompletion, 3); }); test('skips non-completion and other-project activities', () { final completed = TaskActivity( id: 'completed', operationId: 'complete', code: TaskActivityCode.completed, taskId: 'task-1', projectId: 'home', occurredAt: DateTime.utc(2026, 6, 25, 9), metadata: const {'durationMinutes': 25}, ); final pushed = TaskActivity( id: 'pushed', operationId: 'push', code: TaskActivityCode.manuallyPushed, taskId: 'task-1', projectId: 'home', occurredAt: DateTime.utc(2026, 6, 25, 8), ); final otherProject = TaskActivity( id: 'other-project', operationId: 'complete-other', code: TaskActivityCode.completed, taskId: 'task-2', projectId: 'work', occurredAt: DateTime.utc(2026, 6, 25, 10), ); final result = aggregationService.apply( statistics: ProjectStatistics(projectId: 'home'), activities: [completed, pushed, otherProject, completed], ); expect(result.appliedActivityIds, ['completed']); expect(result.statistics.completedTaskCount, 1); expect(result.statistics.durationMinuteCounts, {25: 1}); }); }); group('Project learned suggestions', () { const service = ProjectSuggestionService(); test('uses deterministic samples and resists duration outliers', () { final statistics = ProjectStatistics( projectId: 'home', completedTaskCount: 4, durationMinuteCounts: const {25: 2, 30: 1, 240: 1}, completionTimeBucketCounts: const { ProjectCompletionTimeBucket.morning: 3, ProjectCompletionTimeBucket.evening: 1, }, rewardCounts: const { RewardLevel.high: 2, RewardLevel.low: 1, RewardLevel.notSet: 10, }, difficultyCounts: const { DifficultyLevel.medium: 3, DifficultyLevel.hard: 1, }, reminderProfileCounts: const { ReminderProfile.persistent: 3, ReminderProfile.gentle: 1, }, ); final suggestions = service.suggestionsFor(statistics); expect(suggestions.durationMinutes!.value, 25); expect(suggestions.durationMinutes!.sampleSize, 4); expect(suggestions.durationMinutes!.supportingSampleCount, 2); expect( suggestions.completionTimeBucket!.value, ProjectCompletionTimeBucket.morning, ); expect(suggestions.completionTimeBucket!.confidence, ProjectSuggestionConfidence.high); expect(suggestions.reward!.value, RewardLevel.high); expect(suggestions.reward!.sampleSize, 3); expect(suggestions.difficulty!.value, DifficultyLevel.medium); expect(suggestions.reminderProfile!.value, ReminderProfile.persistent); }); test('does not suggest when samples are insufficient', () { final statistics = ProjectStatistics( projectId: 'home', completedTaskCount: 2, durationMinuteCounts: const {25: 2}, completionTimeBucketCounts: const { ProjectCompletionTimeBucket.morning: 2, }, rewardCounts: const {RewardLevel.high: 2}, difficultyCounts: const {DifficultyLevel.medium: 2}, reminderProfileCounts: const {ReminderProfile.gentle: 2}, ); final suggestions = service.suggestionsFor(statistics); expect(suggestions.durationMinutes, isNull); expect(suggestions.completionTimeBucket, isNull); expect(suggestions.reward, isNull); expect(suggestions.difficulty, isNull); expect(suggestions.reminderProfile, isNull); }); test('does not choose categorical suggestions on ties', () { final statistics = ProjectStatistics( projectId: 'home', completedTaskCount: 4, completionTimeBucketCounts: const { ProjectCompletionTimeBucket.morning: 2, ProjectCompletionTimeBucket.evening: 2, }, rewardCounts: const { RewardLevel.high: 2, RewardLevel.low: 2, }, difficultyCounts: const { DifficultyLevel.easy: 2, DifficultyLevel.hard: 2, }, reminderProfileCounts: const { ReminderProfile.gentle: 2, ReminderProfile.persistent: 2, }, ); final suggestions = service.suggestionsFor(statistics); expect(suggestions.completionTimeBucket, isNull); expect(suggestions.reward, isNull); expect(suggestions.difficulty, isNull); expect(suggestions.reminderProfile, isNull); }); test('stable recomputation is independent of observation order', () { final first = ProjectStatistics(projectId: 'home') .recordCompletion( completedAt: DateTime.utc(2026, 6, 25, 9), durationMinutes: 20, reward: RewardLevel.high, ) .recordCompletion( completedAt: DateTime.utc(2026, 6, 25, 18), durationMinutes: 40, reward: RewardLevel.low, ) .recordCompletion( completedAt: DateTime.utc(2026, 6, 25, 10), durationMinutes: 20, reward: RewardLevel.high, ); final second = ProjectStatistics(projectId: 'home') .recordCompletion( completedAt: DateTime.utc(2026, 6, 25, 18), durationMinutes: 40, reward: RewardLevel.low, ) .recordCompletion( completedAt: DateTime.utc(2026, 6, 25, 10), durationMinutes: 20, reward: RewardLevel.high, ) .recordCompletion( completedAt: DateTime.utc(2026, 6, 25, 9), durationMinutes: 20, reward: RewardLevel.high, ); final firstSuggestions = service.suggestionsFor(first); final secondSuggestions = service.suggestionsFor(second); expect(firstSuggestions.durationMinutes!.value, 20); expect(secondSuggestions.durationMinutes!.value, 20); expect(firstSuggestions.reward!.value, RewardLevel.high); expect(secondSuggestions.reward!.value, RewardLevel.high); }); test('configured defaults remain authoritative beside suggestions', () { final project = ProjectProfile( id: 'home', name: 'Home', colorKey: 'green', defaultReward: RewardLevel.high, defaultDifficulty: DifficultyLevel.hard, defaultReminderProfile: ReminderProfile.strict, defaultDurationMinutes: 45, ); final statistics = ProjectStatistics( projectId: 'home', completedTaskCount: 3, durationMinuteCounts: const {20: 3}, rewardCounts: const {RewardLevel.low: 3}, difficultyCounts: const {DifficultyLevel.easy: 3}, reminderProfileCounts: const {ReminderProfile.gentle: 3}, ); final resolution = service.resolveDefaults( project: project, statistics: statistics, ); expect(resolution.configuredDurationMinutes, 45); expect(resolution.configuredReward, RewardLevel.high); expect(resolution.configuredDifficulty, DifficultyLevel.hard); expect(resolution.configuredReminderProfile, ReminderProfile.strict); expect(resolution.suggestions.durationMinutes!.value, 20); expect(resolution.suggestions.reward!.value, RewardLevel.low); expect(resolution.suggestions.difficulty!.value, DifficultyLevel.easy); expect( resolution.suggestions.reminderProfile!.value, ReminderProfile.gentle, ); expect(project.defaultReward, RewardLevel.high); }); }); } Task scheduledTask({ required String id, required String projectId, required DateTime createdAt, required DateTime start, RewardLevel reward = RewardLevel.notSet, DifficultyLevel difficulty = DifficultyLevel.notSet, ReminderProfile? reminderOverride, }) { return Task( id: id, title: 'Task', projectId: projectId, type: TaskType.flexible, status: TaskStatus.planned, reward: reward, difficulty: difficulty, durationMinutes: 30, scheduledStart: start, scheduledEnd: start.add(const Duration(minutes: 30)), reminderOverride: reminderOverride, createdAt: createdAt, updatedAt: createdAt, ); }