import 'package:flutter_test/flutter_test.dart'; import 'package:tasq/models/programmer_day_sheet.model.dart'; import 'package:tasq/models/programmer_task.model.dart'; import 'package:tasq/models/profile.model.dart'; import 'package:tasq/providers/reports_provider.dart'; import 'package:tasq/screens/programmer_tasks/report/accomplishment_report_data.dart'; import 'package:tasq/utils/programmer_day_rows.dart'; void main() { group('buildAccomplishmentReportData', () { final programmer = Profile( id: 'prog1', role: 'programmer', fullName: 'John Doe', ); final admin = Profile( id: 'admin1', role: 'administrator', fullName: 'Admin User', ); final reviewer = Profile( id: 'reviewer1', role: 'reviewer', fullName: 'Jane Reviewer', ); test('approved-only uses snapshots and counts excluded days', () { final day1 = DateTime.utc(2026, 9, 1); final day2 = DateTime.utc(2026, 9, 2); final day3 = DateTime.utc(2026, 9, 3); final day4 = DateTime.utc(2026, 9, 4); final row1 = DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Software Development', kind: 'assignee', seconds: 3600, notes: [], ); final row2 = DaySheetRow( taskId: 't2', title: 'Task 2', category: 'Bug Fix', kind: 'assignee', seconds: 1800, notes: [], ); // 2 approved sheets final sheets = [ ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row1]), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), ProgrammerDaySheet( id: 'sheet2', programmerId: 'prog1', status: 'approved', workDate: day2, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 1800, rows: [row2]), createdAt: DateTime.utc(2026, 9, 2), updatedAt: DateTime.utc(2026, 9, 2), ), // 1 pending ProgrammerDaySheet( id: 'sheet3', programmerId: 'prog1', status: 'pending', workDate: day3, autoSubmitted: false, resubmissions: 0, createdAt: DateTime.utc(2026, 9, 3), updatedAt: DateTime.utc(2026, 9, 3), ), // 1 disapproved ProgrammerDaySheet( id: 'sheet4', programmerId: 'prog1', status: 'disapproved', workDate: day4, autoSubmitted: false, resubmissions: 1, reviewedBy: 'reviewer1', createdAt: DateTime.utc(2026, 9, 4), updatedAt: DateTime.utc(2026, 9, 4), ), ]; final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 5), label: 'Test Range', ); final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: sheets, liveRowsByDay: {}, tasks: [], profileNames: {'reviewer1': 'Jane Reviewer'}, ); expect(data.days.length, 2); expect(data.excludedPending, 1); expect(data.excludedDisapproved, 1); expect(data.days[0].statusTag, null); expect(data.days[1].statusTag, null); expect(data.totalSeconds, 5400); expect(data.daysReported, 2); }); test('include unapproved tags days and uses live rows', () { final day1 = DateTime.utc(2026, 9, 1); final day2 = DateTime.utc(2026, 9, 2); final day3 = DateTime.utc(2026, 9, 3); final day4 = DateTime.utc(2026, 9, 4); final approvedRow = DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Software Development', kind: 'assignee', seconds: 3600, notes: [], ); final pendingRow = DaySheetRow( taskId: 't2', title: 'Task 2', category: 'Bug Fix', kind: 'assignee', seconds: 1800, notes: [], ); final disapprovedRow = DaySheetRow( taskId: 't3', title: 'Task 3', category: 'Meeting', kind: 'assignee', seconds: 900, notes: [], ); final draftRow = DaySheetRow( taskId: 't4', title: 'Task 4', category: 'Software Support', kind: 'assignee', seconds: 600, notes: [], ); final sheets = [ // approved ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [approvedRow]), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), // pending ProgrammerDaySheet( id: 'sheet2', programmerId: 'prog1', status: 'pending', workDate: day2, autoSubmitted: false, resubmissions: 0, createdAt: DateTime.utc(2026, 9, 2), updatedAt: DateTime.utc(2026, 9, 2), ), // disapproved ProgrammerDaySheet( id: 'sheet3', programmerId: 'prog1', status: 'disapproved', workDate: day3, autoSubmitted: false, resubmissions: 1, reviewedBy: 'reviewer1', createdAt: DateTime.utc(2026, 9, 3), updatedAt: DateTime.utc(2026, 9, 3), ), // draft ProgrammerDaySheet( id: 'sheet4', programmerId: 'prog1', status: 'draft', workDate: day4, autoSubmitted: false, resubmissions: 0, createdAt: DateTime.utc(2026, 9, 4), updatedAt: DateTime.utc(2026, 9, 4), ), ]; final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 5), label: 'Test Range', ); final liveRowsByDay = { day2: [pendingRow], day3: [disapprovedRow], day4: [draftRow], }; final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: true, sheets: sheets, liveRowsByDay: liveRowsByDay, tasks: [], profileNames: {'reviewer1': 'Jane Reviewer'}, ); expect(data.days.length, 4); expect(data.excludedPending, 0); expect(data.excludedDisapproved, 0); expect(data.days[0].statusTag, null); // approved expect(data.days[1].statusTag, 'Pending'); // pending expect(data.days[2].statusTag, 'Disapproved'); // disapproved expect(data.days[3].statusTag, 'Pending'); // draft }); test('period end is inclusive', () { final day1 = DateTime.utc(2026, 9, 1); final row = DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Software Development', kind: 'assignee', seconds: 3600, notes: [], ); final sheets = [ ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), ]; final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 10, 1), // exclusive label: 'Test Range', ); final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: sheets, liveRowsByDay: {}, tasks: [], profileNames: {'reviewer1': 'Jane Reviewer'}, ); expect(data.periodStart, DateTime.utc(2026, 9, 1)); expect(data.periodEndInclusive, DateTime.utc(2026, 9, 30)); }); test('approver name only when one reviewer approved every included day', () { final day1 = DateTime.utc(2026, 9, 1); final day2 = DateTime.utc(2026, 9, 2); final day3 = DateTime.utc(2026, 9, 3); final row = DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Software Development', kind: 'assignee', seconds: 3600, notes: [], ); // Test: same reviewer for all approved days final sheets1 = [ ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), ProgrammerDaySheet( id: 'sheet2', programmerId: 'prog1', status: 'approved', workDate: day2, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 2), updatedAt: DateTime.utc(2026, 9, 2), ), ]; final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 4), label: 'Test Range', ); final data1 = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: sheets1, liveRowsByDay: {}, tasks: [], profileNames: {'reviewer1': 'Jane Reviewer'}, ); expect(data1.approverName, 'Jane Reviewer'); // Test: mixed reviewers final sheets2 = [ ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), ProgrammerDaySheet( id: 'sheet2', programmerId: 'prog1', status: 'approved', workDate: day2, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer2', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 2), updatedAt: DateTime.utc(2026, 9, 2), ), ]; final data2 = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: sheets2, liveRowsByDay: {}, tasks: [], profileNames: {'reviewer1': 'Jane Reviewer', 'reviewer2': 'Bob Reviewer'}, ); expect(data2.approverName, null); }); test('tasksCompleted counts tasks completed in range for the programmer', () { final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 5), label: 'Test Range', ); final task1 = ProgrammerTask( id: 't1', title: 'Task 1', category: 'Software Development', status: 'completed', assigneeId: 'prog1', createdAt: DateTime.utc(2026, 9, 1), completedAt: DateTime.utc(2026, 9, 3), // in range updatedAt: DateTime.utc(2026, 9, 3), ); final task2 = ProgrammerTask( id: 't2', title: 'Task 2', category: 'Bug Fix', status: 'completed', assigneeId: 'prog1', createdAt: DateTime.utc(2026, 9, 1), completedAt: DateTime.utc(2026, 8, 31), // before range updatedAt: DateTime.utc(2026, 8, 31), ); final task3 = ProgrammerTask( id: 't3', title: 'Task 3', category: 'Meeting', status: 'completed', assigneeId: 'prog2', // different assignee createdAt: DateTime.utc(2026, 9, 1), completedAt: DateTime.utc(2026, 9, 3), updatedAt: DateTime.utc(2026, 9, 3), ); final task4 = ProgrammerTask( id: 't4', title: 'Task 4', category: 'Software Support', status: 'in_progress', // not completed assigneeId: 'prog1', createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 3), ); final task5 = ProgrammerTask( id: 't5', title: 'Task 5', category: 'Software Development', status: 'completed', assigneeId: 'prog1', createdAt: DateTime.utc(2026, 9, 1), completedAt: DateTime.utc(2026, 9, 5), // at end boundary (exclusive) updatedAt: DateTime.utc(2026, 9, 5), ); final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: [], liveRowsByDay: {}, tasks: [task1, task2, task3, task4, task5], profileNames: {}, ); expect(data.tasksCompleted, 1); // only task1 }); test('empty period', () { final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 2), label: 'Test Range', ); final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: [], liveRowsByDay: {}, tasks: [], profileNames: {}, ); expect(data.days, isEmpty); expect(data.daysReported, 0); expect(data.totalSeconds, 0); expect(data.excludedPending, 0); expect(data.excludedDisapproved, 0); }); test('positionLabel based on programmer role', () { final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 2), label: 'Test Range', ); final dataProgrammer = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: [], liveRowsByDay: {}, tasks: [], profileNames: {}, ); final dataAdmin = buildAccomplishmentReportData( programmer: admin, range: range, includeUnapproved: false, sheets: [], liveRowsByDay: {}, tasks: [], profileNames: {}, ); expect(dataProgrammer.positionLabel, 'Programmer'); expect(dataAdmin.positionLabel, 'Administrator'); }); test('helpSeconds and secondsByCategory calculations', () { final day1 = DateTime.utc(2026, 9, 1); final assigneeRow = DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Software Development', kind: 'assignee', seconds: 3600, notes: [], ); final helperRow = DaySheetRow( taskId: 't2', title: 'Task 2', category: 'Bug Fix', kind: 'helper', seconds: 1800, notes: [], ); final anotherHelperRow = DaySheetRow( taskId: 't3', title: 'Task 3', category: 'Software Development', kind: 'helper', seconds: 900, notes: [], ); final row = DaySheetRow( taskId: 't4', title: 'Task 4', category: 'Meeting', kind: 'assignee', seconds: 600, notes: [], ); final sheets = [ ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot( totalSeconds: 6900, rows: [assigneeRow, helperRow, anotherHelperRow, row], ), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), ]; final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 2), label: 'Test Range', ); final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: sheets, liveRowsByDay: {}, tasks: [], profileNames: {'reviewer1': 'Jane Reviewer'}, ); expect(data.helpSeconds, 2700); // 1800 + 900 expect(data.secondsByCategory['Software Development'], 4500); // 3600 + 900 expect(data.secondsByCategory['Bug Fix'], 1800); expect(data.secondsByCategory['Meeting'], 600); expect(data.totalSeconds, 6900); }); test('approved sheet with zero rows and different reviewer does not affect approverName', () { final day1 = DateTime.utc(2026, 9, 1); final day2 = DateTime.utc(2026, 9, 2); final day3 = DateTime.utc(2026, 9, 3); final row = DaySheetRow( taskId: 't1', title: 'Task 1', category: 'Software Development', kind: 'assignee', seconds: 3600, notes: [], ); // Two included approved sheets with same reviewer // One zero-row approved sheet with different reviewer final sheets = [ ProgrammerDaySheet( id: 'sheet1', programmerId: 'prog1', status: 'approved', workDate: day1, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 1), updatedAt: DateTime.utc(2026, 9, 1), ), // Zero-row approved sheet with different reviewer (should be ignored) ProgrammerDaySheet( id: 'sheet2', programmerId: 'prog1', status: 'approved', workDate: day2, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer2', approvedSnapshot: DaySheetSnapshot(totalSeconds: 0, rows: []), createdAt: DateTime.utc(2026, 9, 2), updatedAt: DateTime.utc(2026, 9, 2), ), ProgrammerDaySheet( id: 'sheet3', programmerId: 'prog1', status: 'approved', workDate: day3, autoSubmitted: false, resubmissions: 0, reviewedBy: 'reviewer1', approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]), createdAt: DateTime.utc(2026, 9, 3), updatedAt: DateTime.utc(2026, 9, 3), ), ]; final range = ReportDateRange( start: DateTime.utc(2026, 9, 1), end: DateTime.utc(2026, 9, 4), label: 'Test Range', ); final data = buildAccomplishmentReportData( programmer: programmer, range: range, includeUnapproved: false, sheets: sheets, liveRowsByDay: {}, tasks: [], profileNames: { 'reviewer1': 'Jane Reviewer', 'reviewer2': 'Bob Reviewer', }, ); // Only 2 days included (day2 has zero rows) expect(data.days.length, 2); // Both included days were reviewed by reviewer1 expect(data.approverName, 'Jane Reviewer'); }); }); }