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/models/programmer_task_work_log.model.dart'; import 'package:tasq/utils/app_time.dart'; import 'package:tasq/utils/programmer_day_rows.dart'; import 'package:tasq/widgets/quill_field.dart'; void main() { setUpAll(AppTime.initialize); group('deltaJsonToPlainText', () { test('legacy plain text notes pass through', () { expect(deltaJsonToPlainText('fixed bug'), 'fixed bug'); expect(deltaJsonToPlainText(' spaces '), 'spaces'); expect(deltaJsonToPlainText(''), ''); }); test('null returns empty string', () { expect(deltaJsonToPlainText(null), ''); }); test('invalid JSON falls back to returning trimmed string', () { expect(deltaJsonToPlainText('[{invalid json'), '[{invalid json'); }); test('Delta JSON with insert is converted to plain text', () { expect(deltaJsonToPlainText('[{"insert":"Hello\\n"}]'), 'Hello'); }); test('Delta JSON arrays starting with [ are attempted to be parsed', () { // The function attempts to parse JSON that starts with '[' // If parsing fails, it returns the trimmed string // This test verifies the fallback behavior expect(deltaJsonToPlainText('[{"invalid":"json"}]'), '[{"invalid":"json"}]'); }); }); group('DaySheetRow', () { test('json round-trip', () { final row = DaySheetRow( taskId: 't1', title: 'Review PR', category: 'Software Support', kind: 'assignee', projectName: 'Project A', helpedName: null, seconds: 3600, notes: ['First note', 'Second note'], ); expect(DaySheetRow.fromJson(row.toJson()).toJson(), row.toJson()); }); test('helper row json round-trip', () { final row = DaySheetRow( taskId: 't2', title: 'Debug issue', category: 'Bug Fix', kind: 'helper', projectName: null, helpedName: 'Ana Cruz', seconds: 2700, notes: ['Helped with testing'], ); expect(DaySheetRow.fromJson(row.toJson()).toJson(), row.toJson()); }); }); group('ProgrammerTaskWorkLog.workDate', () { test('workDate defaults to Manila day of createdAt', () { final base = { 'id': 'l1', 'task_id': 't1', 'author_id': 'a', 'description': 'work', 'minutes': null, 'created_at': '2026-09-26T16:30:00Z', }; final log = ProgrammerTaskWorkLog.fromMap(base); // 2026-09-26 16:30 UTC is 2026-09-27 in Manila (UTC+8) expect(log.workDate, DateTime.utc(2026, 9, 27)); }); test('workDate from map when provided', () { final base = { 'id': 'l1', 'task_id': 't1', 'author_id': 'a', 'description': 'work', 'minutes': null, 'created_at': '2026-09-26T16:30:00Z', 'work_date': '2026-09-25', }; final log = ProgrammerTaskWorkLog.fromMap(base); expect(log.workDate, DateTime.utc(2026, 9, 25)); }); test('workDate constructor parameter', () { final log = ProgrammerTaskWorkLog( id: 'l1', taskId: 't1', authorId: 'a', description: 'work', createdAt: DateTime.utc(2026, 9, 26, 16, 30), workDate: DateTime.utc(2026, 9, 25), ); expect(log.workDate, DateTime.utc(2026, 9, 25)); }); }); group('buildDayRows', () { final day = DateTime.utc(2026, 9, 27); final earlierDay = DateTime.utc(2026, 9, 26); final now = DateTime.utc(2026, 9, 27, 12, 0); ProgrammerTask _task(String id, String assigneeId, [String? projectId]) => ProgrammerTask( id: id, title: 'Task $id', category: 'Software Support', status: 'in_progress', assigneeId: assigneeId, creatorId: 'creator', projectId: projectId, createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 27), ); ProgrammerTaskActivityLog _log(String taskId, String type, [Map? meta]) => ProgrammerTaskActivityLog( id: '$taskId-$type', taskId: taskId, actionType: type, meta: meta, createdAt: DateTime.utc(2026, 9, 27, 10), ); ProgrammerTaskWorkLog _workLog( String taskId, String authorId, String description, { int? minutes, DateTime? workDate, }) => ProgrammerTaskWorkLog( id: '$taskId-$authorId-${minutes ?? "notes"}', taskId: taskId, authorId: authorId, description: description, minutes: minutes, createdAt: DateTime.utc(2026, 9, 27, 10), workDate: workDate, ); test('assignee row includes only that-day seconds and notes', () { // Task t1 assigned to 'a' // 'a' worked 1 hour on t1 that day // 'a' also logged a note on t1 that day // 'a' logged a note on t1 the day before (should be excluded) final task = _task('t1', 'a'); final workLogs = [ _workLog('t1', 'a', 'Fixed part 1', workDate: day), _workLog('t1', 'a', 'Fixed part 2', workDate: earlierDay), ]; final rows = buildDayRows( userId: 'a', day: day, tasks: [task], logsByTask: { 't1': [ ProgrammerTaskActivityLog( id: 't1-started', taskId: 't1', actionType: 'started', createdAt: DateTime.utc(2026, 9, 27, 8, 0), ), ProgrammerTaskActivityLog( id: 't1-paused', taskId: 't1', actionType: 'paused', createdAt: DateTime.utc(2026, 9, 27, 9, 0), ), ], }, workLogs: workLogs, projectNames: {}, profileNames: {'a': 'Alice Smith'}, now: now, ); expect(rows.length, 1); expect(rows[0].kind, 'assignee'); expect(rows[0].notes, ['Fixed part 1']); }); test('helper row: minutes*60 and helpedName', () { // Task t1 assigned to 'a' // 'h' (helper) logs 45 minutes of help on t1 that day final task = _task('t1', 'a'); final workLogs = [ _workLog('t1', 'h', 'Helped with debugging', minutes: 45, workDate: day), ]; final rows = buildDayRows( userId: 'h', day: day, tasks: [task], logsByTask: {'t1': []}, workLogs: workLogs, projectNames: {}, profileNames: {'a': 'Ana Cruz'}, now: now, ); expect(rows.length, 1); expect(rows[0].kind, 'helper'); expect(rows[0].seconds, 2700); expect(rows[0].helpedName, 'Ana Cruz'); }); test('notes-only row when assignee logged but ran no timer', () { // Task t1 assigned to 'a' // 'a' only logged notes (no timer activity) final task = _task('t1', 'a'); final workLogs = [ _workLog('t1', 'a', 'Just some notes', workDate: day), ]; final rows = buildDayRows( userId: 'a', day: day, tasks: [task], logsByTask: {'t1': []}, workLogs: workLogs, projectNames: {}, profileNames: {'a': 'Alice Smith'}, now: now, ); expect(rows.length, 1); expect(rows[0].kind, 'assignee'); expect(rows[0].seconds, 0); expect(rows[0].notes, ['Just some notes']); }); test('rows sort: assignee by seconds desc, then helpers by seconds desc', () { // Task t1 assigned to 'a': 1 hour // Task t2 assigned to 'a': 30 minutes // Task t3 assigned to 'b': with 'a' as helper (20 minutes) // Task t4 assigned to 'b': with 'a' as helper (10 minutes) final tasks = [ _task('t1', 'a'), _task('t2', 'a'), _task('t3', 'b'), _task('t4', 'b'), ]; final workLogs = [ _workLog('t3', 'a', 'helped b', minutes: 20, workDate: day), _workLog('t4', 'a', 'helped b', minutes: 10, workDate: day), ]; final rows = buildDayRows( userId: 'a', day: day, tasks: tasks, logsByTask: { 't1': [ ProgrammerTaskActivityLog( id: 't1-started', taskId: 't1', actionType: 'started', createdAt: DateTime.utc(2026, 9, 27, 8, 0), ), ProgrammerTaskActivityLog( id: 't1-paused', taskId: 't1', actionType: 'paused', createdAt: DateTime.utc(2026, 9, 27, 9, 0), ), ], 't2': [ ProgrammerTaskActivityLog( id: 't2-started', taskId: 't2', actionType: 'started', createdAt: DateTime.utc(2026, 9, 27, 9, 0), ), ProgrammerTaskActivityLog( id: 't2-paused', taskId: 't2', actionType: 'paused', createdAt: DateTime.utc(2026, 9, 27, 9, 30), ), ], 't3': [], 't4': [], }, workLogs: workLogs, projectNames: {}, profileNames: {'a': 'Alice Smith', 'b': 'Bob Jones'}, now: now, ); // Expect: assignee t1 (3600), assignee t2 (1800), helper t3 (1200), helper t4 (600) expect(rows.length, 4); expect(rows[0].kind, 'assignee'); expect(rows[0].taskId, 't1'); expect(rows[0].seconds, 3600); expect(rows[1].kind, 'assignee'); expect(rows[1].taskId, 't2'); expect(rows[1].seconds, 1800); expect(rows[2].kind, 'helper'); expect(rows[2].taskId, 't3'); expect(rows[2].seconds, 1200); expect(rows[2].helpedName, 'Bob Jones'); expect(rows[3].kind, 'helper'); expect(rows[3].taskId, 't4'); expect(rows[3].seconds, 600); expect(rows[3].helpedName, 'Bob Jones'); }); test('tasks missing from task list are skipped', () { // Work logs reference t1, but t1 is not in tasks list final workLogs = [ _workLog('t1', 'a', 'work', workDate: day), ]; final rows = buildDayRows( userId: 'a', day: day, tasks: [], logsByTask: {}, workLogs: workLogs, projectNames: {}, profileNames: {}, now: now, ); expect(rows, isEmpty); }); test('helper logs with minutes=0 are ignored', () { final task = _task('t1', 'a'); final workLogs = [ _workLog('t1', 'h', 'zero minutes', minutes: 0, workDate: day), ]; final rows = buildDayRows( userId: 'h', day: day, tasks: [task], logsByTask: {'t1': []}, workLogs: workLogs, projectNames: {}, profileNames: {'a': 'Ana Cruz'}, now: now, ); expect(rows, isEmpty); }); test('empty notes are dropped from output', () { final task = _task('t1', 'a'); final workLogs = [ _workLog('t1', 'a', ' ', workDate: day), // whitespace-only _workLog('t1', 'a', 'Real note', workDate: day), _workLog('t1', 'a', '', workDate: day), // empty ]; final rows = buildDayRows( userId: 'a', day: day, tasks: [task], logsByTask: {'t1': []}, workLogs: workLogs, projectNames: {}, profileNames: {}, now: now, ); expect(rows.length, 1); expect(rows[0].notes, ['Real note']); }); }); group('totalSeconds', () { test('sums all row seconds', () { final rows = [ DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Work', kind: 'assignee', seconds: 3600, notes: [], ), DaySheetRow( taskId: 't2', title: 'Task 2', category: 'Work', kind: 'helper', seconds: 1800, notes: [], helpedName: 'Someone', ), ]; expect(totalSeconds(rows), 5400); }); test('empty list returns 0', () { expect(totalSeconds([]), 0); }); }); }