import 'package:flutter_test/flutter_test.dart'; import 'package:tasq/models/programmer_task.model.dart'; import 'package:tasq/models/programmer_task_activity_log.model.dart'; import 'package:tasq/utils/app_time.dart'; import 'package:tasq/utils/programmer_daily_time.dart'; void main() { setUpAll(AppTime.initialize); /// Helper to create a ProgrammerTaskActivityLog at Manila time d h:m ProgrammerTaskActivityLog _ev( String action, int d, int h, [ int m = 0, Map? meta, ]) { return ProgrammerTaskActivityLog( id: 'ev-$d-$h-$m', taskId: 'task-1', actorId: 'actor', actionType: action, meta: meta, createdAt: AppTime.fromComponents( year: 2026, month: 9, day: d, hour: h, minute: m, ), ); } /// Helper to create a ProgrammerTask ProgrammerTask _task({ String status = ProgrammerTaskStatus.inProgress, String? creatorId = 'a', String? assigneeId = 'a', DateTime? startedAt, DateTime? completedAt, DateTime? cancelledAt, }) { return ProgrammerTask( id: 'task-1', title: 'Test Task', category: ProgrammerTaskCategory.softwareDevelopment, status: status, creatorId: creatorId, assigneeId: assigneeId, createdAt: AppTime.fromComponents(year: 2026, month: 9, day: 27), startedAt: startedAt, completedAt: completedAt, cancelledAt: cancelledAt, updatedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27), ); } test('manilaDay ignores device time zone', () { // Sep 26 4:30 PM UTC = Sep 27 12:30 AM Manila time expect( manilaDay(DateTime.parse('2026-09-26T16:30:00Z')), DateTime.utc(2026, 9, 27), ); // Sep 26 3:59 PM UTC = Sep 26 11:59 PM Manila time expect( manilaDay(DateTime.parse('2026-09-26T15:59:00Z')), DateTime.utc(2026, 9, 26), ); }); test('day key round-trip', () { expect(formatDayKey(parseDayKey('2026-09-27')), '2026-09-27'); expect(formatDayKey(parseDayKey('2025-01-05')), '2025-01-05'); }); test('single day with a pause', () { final task = _task( status: ProgrammerTaskStatus.completed, creatorId: 'a', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 15), ); final logs = [ _ev('started', 27, 9), _ev('paused', 27, 11), _ev('resumed', 27, 13), _ev('completed', 27, 15), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result['a'], {DateTime.utc(2026, 9, 27): 4 * 3600}); }); test('running across Manila midnight splits by day', () { final task = _task( status: ProgrammerTaskStatus.inProgress, creatorId: 'a', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 26, hour: 22), ); final logs = [ _ev('started', 26, 22), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 2); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result['a']![DateTime.utc(2026, 9, 26)], 2 * 3600); expect(result['a']![DateTime.utc(2026, 9, 27)], 2 * 3600); }); test('reassignment credits each assignee for their own span', () { final task = _task( status: ProgrammerTaskStatus.completed, creatorId: 'a', assigneeId: 'b', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 12), ); final logs = [ _ev('started', 27, 9), _ev('reassigned', 27, 10, 0, {'assignee_id': 'b'}), _ev('completed', 27, 12), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); expect(result['b']![DateTime.utc(2026, 9, 27)], 2 * 3600); }); test('initial assignee comes from the first assigned event', () { final task = _task( status: ProgrammerTaskStatus.completed, creatorId: 'boss', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 10), ); final logs = [ _ev('assigned', 27, 8, 0, {'assignee_id': 'a'}), _ev('started', 27, 9), _ev('completed', 27, 10), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result.keys.toList(), ['a']); expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); }); test('adjustment subtracts on the day created and clamps at zero', () { // Case 1: adjustment {seconds:1800} at 10:00 final task1 = _task( status: ProgrammerTaskStatus.completed, creatorId: 'a', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 11), ); final logs1 = [ _ev('started', 27, 9), _ev('adjustment', 27, 10, 0, {'seconds': 1800}), _ev('completed', 27, 11), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20); final result1 = dailySecondsByAssignee(task: task1, logs: logs1, now: now); expect(result1['a']![DateTime.utc(2026, 9, 27)], 5400); // 7200 - 1800 // Case 2: adjustment {seconds: 99999} -> clamps at 0 final task2 = _task( status: ProgrammerTaskStatus.completed, creatorId: 'a', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 11), ); final logs2 = [ _ev('started', 27, 9), _ev('adjustment', 27, 10, 0, {'seconds': 99999}), _ev('completed', 27, 11), ]; final result2 = dailySecondsByAssignee(task: task2, logs: logs2, now: now); expect(result2['a']![DateTime.utc(2026, 9, 27)], 0); // clamped }); test('falls back to task.startedAt when there is no started log', () { final task = _task( status: ProgrammerTaskStatus.completed, creatorId: 'a', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 10), ); final logs = [ _ev('completed', 27, 10), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); }); test('a paused in-progress task stops accruing at the pause', () { final task = _task( status: ProgrammerTaskStatus.inProgress, creatorId: 'a', assigneeId: 'a', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), ); final logs = [ _ev('started', 27, 9), _ev('paused', 27, 10), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 15); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); // not 6 hours }); test('reassignment while paused does not open a new segment for the new assignee', () { // task: started 09:00, paused 10:00, reassigned {assignee_id:'b'} 11:00, completed 12:00 (all Sep 27) // expected: a -> 3600s (09-10), b -> 0 (no interval opened after paused reassignment) final task = _task( status: ProgrammerTaskStatus.completed, creatorId: 'a', assigneeId: 'b', startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9), completedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 12), ); final logs = [ _ev('started', 27, 9), _ev('paused', 27, 10), _ev('reassigned', 27, 11, 0, {'assignee_id': 'b'}), _ev('completed', 27, 12), ]; final now = AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20); final result = dailySecondsByAssignee(task: task, logs: logs, now: now); expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); // a worked 09-10 // b should have no time, or null entry expect( result['b']?[DateTime.utc(2026, 9, 27)] ?? 0, 0, ); }); }