import 'dart:async'; 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/providers/programmer_day_data_provider.dart'; import 'package:tasq/utils/app_time.dart'; // --------------------------------------------------------------------------- // Minimal fake Supabase client — only the surface fetchDayActivityData uses. // --------------------------------------------------------------------------- class _FakeClient { _FakeClient({required this.tables}); final Map>> tables; _FakeQuery from(String table) => _FakeQuery(this, table); } class _FakeQuery implements Future>> { _FakeQuery(this._client, this._table); final _FakeClient _client; final String _table; final List> _eqs = []; String? _inField; List? _inValues; String? _orderField; bool _orderAsc = true; String? _gteField; dynamic _gteValue; String? _ltField; dynamic _ltValue; List> get _rows { var rows = List>.from(_client.tables[_table] ?? []); for (final eq in _eqs) { rows = rows.where((r) => r[eq.key] == eq.value).toList(); } if (_inField != null && _inValues != null) { rows = rows .where((r) => _inValues!.contains(r[_inField]?.toString())) .toList(); } if (_gteField != null) { rows = rows.where((r) { final v = r[_gteField]; return v != null && v.toString().compareTo(_gteValue.toString()) >= 0; }).toList(); } if (_ltField != null) { rows = rows.where((r) { final v = r[_ltField]; return v != null && v.toString().compareTo(_ltValue.toString()) < 0; }).toList(); } if (_orderField != null) { rows.sort((a, b) { final av = '${a[_orderField]}'; final bv = '${b[_orderField]}'; return _orderAsc ? av.compareTo(bv) : bv.compareTo(av); }); } return rows; } Future>> get _future async => _rows; _FakeQuery select([String? _]) => this; _FakeQuery eq(String field, dynamic value) { _eqs.add(MapEntry(field, value)); return this; } _FakeQuery inFilter(String field, List values) { _inField = field; _inValues = values.map((v) => v.toString()).toList(); return this; } _FakeQuery gte(String field, dynamic value) { _gteField = field; _gteValue = value; return this; } _FakeQuery lt(String field, dynamic value) { _ltField = field; _ltValue = value; return this; } _FakeQuery order(String field, {bool ascending = true}) { _orderField = field; _orderAsc = ascending; return this; } @override Stream>> asStream() => _future.asStream(); @override Future>> catchError(Function onError, {bool Function(Object error)? test}) => _future.catchError(onError, test: test); @override Future then( FutureOr Function(List>) onValue, {Function? onError}) => _future.then(onValue, onError: onError); @override Future>> timeout(Duration timeLimit, {FutureOr>> Function()? onTimeout}) => _future.timeout(timeLimit, onTimeout: onTimeout); @override Future>> whenComplete( FutureOr Function() action) => _future.whenComplete(action); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- ProgrammerTask _task({ required String id, required String status, DateTime? startedAt, DateTime? completedAt, DateTime? cancelledAt, }) { return ProgrammerTask( id: id, title: 'Task $id', category: ProgrammerTaskCategory.others, status: status, createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), startedAt: startedAt, completedAt: completedAt, cancelledAt: cancelledAt, ); } ProgrammerTaskActivityLog _log({ required String taskId, required String actionType, required DateTime createdAt, }) { return ProgrammerTaskActivityLog( id: '${taskId}_${actionType}_${createdAt.millisecondsSinceEpoch}', taskId: taskId, actionType: actionType, createdAt: createdAt, ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { setUpAll(AppTime.initialize); group('computeRunStates', () { test( 'queued->notStarted; latest paused->paused since pause time; ' 'resumed->running; completed', () { final startedAt = DateTime.utc(2026, 9, 27, 8, 0); final pauseAt = DateTime.utc(2026, 9, 27, 10, 0); final resumeAt = DateTime.utc(2026, 9, 27, 11, 0); final tasks = [ _task(id: 'q', status: ProgrammerTaskStatus.queued), _task( id: 'p', status: ProgrammerTaskStatus.inProgress, startedAt: startedAt, ), _task( id: 'r', status: ProgrammerTaskStatus.inProgress, startedAt: DateTime.utc(2026, 9, 27, 9, 0), ), _task( id: 'c', status: ProgrammerTaskStatus.completed, startedAt: startedAt, completedAt: DateTime.utc(2026, 9, 27, 12, 0), ), ]; final logsByTask = { 'p': [ _log(taskId: 'p', actionType: 'started', createdAt: startedAt), _log(taskId: 'p', actionType: 'paused', createdAt: pauseAt), ], 'r': [ _log(taskId: 'r', actionType: 'started', createdAt: DateTime.utc(2026, 9, 27, 9, 0)), _log(taskId: 'r', actionType: 'paused', createdAt: DateTime.utc(2026, 9, 27, 10, 30)), _log(taskId: 'r', actionType: 'resumed', createdAt: resumeAt), ], }; final states = computeRunStates(tasks, logsByTask); // queued -> notStarted expect(states['q']!.state, ProgrammerRunState.notStarted); expect(states['q']!.since, isNull); // latest action is paused -> paused since pause time expect(states['p']!.state, ProgrammerRunState.paused); expect(states['p']!.since, pauseAt); // latest action is resumed -> running since resume time expect(states['r']!.state, ProgrammerRunState.running); expect(states['r']!.since, resumeAt); // completed -> completed expect(states['c']!.state, ProgrammerRunState.completed); expect(states['c']!.since, isNull); }); }); group('fetchDayActivityData', () { // Target day: 2026-09-27 Manila. // dayStart (Manila midnight) = 2026-09-26T16:00:00Z UTC. // dayEnd = 2026-09-27T16:00:00Z UTC. final day = DateTime.utc(2026, 9, 27); final now = DateTime.utc(2026, 9, 27, 12, 0); test('includes in-progress tasks and tasks completed that day', () async { final tActive = _task( id: 'active', status: ProgrammerTaskStatus.inProgress, startedAt: DateTime.utc(2026, 9, 27, 0, 0), ); // Completed at 11:00 UTC on the 27th — still within the Manila day final tDone = _task( id: 'done', status: ProgrammerTaskStatus.completed, startedAt: DateTime.utc(2026, 9, 27, 0, 0), completedAt: DateTime.utc(2026, 9, 27, 3, 0), ); final client = _FakeClient(tables: { 'programmer_task_work_logs': [], 'programmer_task_activity_logs': [ { 'id': 'log-1', 'task_id': 'active', 'action_type': 'started', 'created_at': '2026-09-27T00:00:00Z', }, ], }); final result = await fetchDayActivityData( client, day: day, tasks: [tActive, tDone], now: now, ); expect(result.tasks.map((t) => t.id), containsAll(['active', 'done'])); }); test('excludes a task completed two days earlier', () async { // Completed well before the Manila day boundary (2026-09-26T16:00Z). final tOld = _task( id: 'old', status: ProgrammerTaskStatus.completed, startedAt: DateTime.utc(2026, 9, 24, 8, 0), completedAt: DateTime.utc(2026, 9, 25, 10, 0), ); final client = _FakeClient(tables: { 'programmer_task_work_logs': [], 'programmer_task_activity_logs': [], }); final result = await fetchDayActivityData( client, day: day, tasks: [tOld], now: now, ); expect(result.tasks, isEmpty); }); test('includes a task referenced only by a helper work log that day', () async { // Task was completed long before; not active on 2026-09-27 by dates alone. final tHelper = _task( id: 'helper-task', status: ProgrammerTaskStatus.completed, startedAt: DateTime.utc(2026, 9, 1, 8, 0), completedAt: DateTime.utc(2026, 9, 1, 17, 0), ); final client = _FakeClient(tables: { 'programmer_task_work_logs': [ { 'id': 'wl-1', 'task_id': 'helper-task', 'author_id': 'u2', 'description': 'helped out', 'minutes': 30, 'created_at': '2026-09-27T01:00:00Z', 'work_date': '2026-09-27', }, ], 'programmer_task_activity_logs': [], }); final result = await fetchDayActivityData( client, day: day, tasks: [tHelper], now: now, ); expect(result.tasks.map((t) => t.id), contains('helper-task')); expect(result.workLogs, hasLength(1)); expect(result.workLogs.first.taskId, 'helper-task'); }); test('excludes work logs with a different work_date', () async { final tTask = _task( id: 'some-task', status: ProgrammerTaskStatus.inProgress, startedAt: DateTime.utc(2026, 9, 27, 0, 0), ); final client = _FakeClient(tables: { 'programmer_task_work_logs': [ { 'id': 'wl-old', 'task_id': 'some-task', 'author_id': 'u1', 'description': 'yesterday', 'minutes': 60, 'created_at': '2026-09-26T10:00:00Z', 'work_date': '2026-09-26', }, ], 'programmer_task_activity_logs': [], }); final result = await fetchDayActivityData( client, day: day, tasks: [tTask], now: now, ); expect(result.workLogs, isEmpty); }); }); }