import 'package:flutter/material.dart'; 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/utils/app_time.dart'; import 'package:tasq/utils/programmer_stats.dart'; void main() { setUpAll(AppTime.initialize); ProgrammerDaySheet makeSheet({ String id = 'id', String programmerId = 'user1', String status = 'pending', required DateTime workDate, int resubmissions = 0, DaySheetSnapshot? approvedSnapshot, }) { return ProgrammerDaySheet( id: id, programmerId: programmerId, status: status, workDate: workDate, resubmissions: resubmissions, approvedSnapshot: approvedSnapshot, createdAt: DateTime.utc(2026, 1, 1), updatedAt: DateTime.utc(2026, 1, 1), ); } ProgrammerStats statsWith({ int completedTotal = 0, int bugFixCompleted = 0, Duration approvedTotal = Duration.zero, Duration helpGivenTotal = Duration.zero, Duration helpGivenThisMonth = Duration.zero, Duration todayFocus = Duration.zero, int completedThisWeek = 0, int completedLastWeek = 0, int currentStreak = 0, int bestStreak = 0, double? firstPassRate, bool hasCleanWeek = false, }) { return ProgrammerStats( todayFocus: todayFocus, helpGivenThisMonth: helpGivenThisMonth, approvedTotal: approvedTotal, helpGivenTotal: helpGivenTotal, completedThisWeek: completedThisWeek, completedLastWeek: completedLastWeek, currentStreak: currentStreak, bestStreak: bestStreak, completedTotal: completedTotal, bugFixCompleted: bugFixCompleted, firstPassRate: firstPassRate, hasCleanWeek: hasCleanWeek, ); } test('fromMap parses work_date and snapshot', () { final ts = DateTime.utc(2026, 9, 28).toIso8601String(); final m = { 'id': 'ds1', 'programmer_id': 'user1', 'status': 'approved', 'work_date': '2026-09-23', 'submitted_at': null, 'reviewed_at': null, 'auto_submitted': false, 'reviewed_by': null, 'resubmissions': 0, 'approved_snapshot': { 'v': 1, 'total_seconds': 3600, 'rows': [], }, 'created_at': ts, 'updated_at': ts, }; final sheet = ProgrammerDaySheet.fromMap(m); expect(sheet.workDate, DateTime.utc(2026, 9, 23)); expect(sheet.approvedSnapshot?.totalSeconds, 3600); expect(sheet.isFirstPassApproved, isTrue); }); group('computeStreak', () { test('streak counts weekdays back from today, skipping weekends', () { // today Mon Sep 28 (no sheet); // sheets: Wed Sep 23 pending, Thu Sep 24 pending, Fri Sep 25 approved final sheets = [ makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 23), status: 'pending'), makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 24), status: 'pending'), makeSheet(id: 'c', workDate: DateTime.utc(2026, 9, 25), status: 'approved'), ]; final result = computeStreak( sheets: sheets, leaveDays: {}, today: DateTime.utc(2026, 9, 28), ); expect(result.current, 3); }); test('a disapproved day breaks the streak', () { // Fri Sep 25 disapproved -> streak breaks immediately after skipping today final sheets = [ makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 25), status: 'disapproved'), ]; final result = computeStreak( sheets: sheets, leaveDays: {}, today: DateTime.utc(2026, 9, 28), ); expect(result.current, 0); }); test('approved leave days are neutral', () { // Thu Sep 24 in leaveDays (no sheet); Wed Sep 23 approved, Fri Sep 25 approved; // today Mon Sep 28 (no sheet) -> current 2 final sheets = [ makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 23), status: 'approved'), makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 25), status: 'approved'), ]; final result = computeStreak( sheets: sheets, leaveDays: {DateTime.utc(2026, 9, 24)}, today: DateTime.utc(2026, 9, 28), ); expect(result.current, 2); }); test('best streak finds the longest earlier run', () { // Earlier run of 4: Mon Sep 7, Tue Sep 8, Wed Sep 9, Thu Sep 10 all approved // Then gap (Fri Sep 11 missing), later Fri Sep 25 approved (run=1) // today Mon Sep 28 (no sheet) -> current=1, best=4 final sheets = [ makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 7), status: 'approved'), makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 8), status: 'approved'), makeSheet(id: 'c', workDate: DateTime.utc(2026, 9, 9), status: 'approved'), makeSheet(id: 'd', workDate: DateTime.utc(2026, 9, 10), status: 'approved'), makeSheet(id: 'e', workDate: DateTime.utc(2026, 9, 25), status: 'approved'), ]; final result = computeStreak( sheets: sheets, leaveDays: {}, today: DateTime.utc(2026, 9, 28), ); expect(result.best, 4); }); }); group('firstPassRate', () { test('first-pass rate over 30 days', () { // 2 approved(r=0), 1 approved(r=1), 1 disapproved -> 2 first-pass / 4 reviewed = 0.5 final now = DateTime.utc(2026, 9, 28); final base = DateTime.utc(2026, 9, 20); final sheets = [ makeSheet(id: 'a', workDate: base, status: 'approved', resubmissions: 0), makeSheet( id: 'b', workDate: base.add(const Duration(days: 1)), status: 'approved', resubmissions: 0), makeSheet( id: 'c', workDate: base.add(const Duration(days: 2)), status: 'approved', resubmissions: 1), makeSheet( id: 'd', workDate: base.add(const Duration(days: 3)), status: 'disapproved', resubmissions: 0), ]; final stats = computeProgrammerStats( userId: 'user1', now: now, tasks: [], sheets: sheets, leaveDays: {}, todayFocus: Duration.zero, ); expect(stats.firstPassRate, closeTo(0.5, 0.001)); }); test('no reviewed sheets gives null first-pass rate', () { final stats = computeProgrammerStats( userId: 'user1', now: DateTime.utc(2026, 9, 28), tasks: [], sheets: [ makeSheet( id: 'a', workDate: DateTime.utc(2026, 9, 20), status: 'draft', resubmissions: 0), ], leaveDays: {}, todayFocus: Duration.zero, ); expect(stats.firstPassRate, isNull); }); }); test('completedThisWeek uses a Monday-start week', () { // now = Mon Sep 28, 2026 // week Mon Sep 28 - Sun Oct 4; last week Mon Sep 21 - Sun Sep 27 final now = DateTime.utc(2026, 9, 28); ProgrammerTask makeTask(String id, DateTime completedAt) { return ProgrammerTask( id: id, title: 'Task $id', category: ProgrammerTaskCategory.others, status: ProgrammerTaskStatus.completed, assigneeId: 'user1', createdAt: DateTime.utc(2026, 1, 1), updatedAt: DateTime.utc(2026, 1, 1), completedAt: completedAt, ); } final tasks = [ makeTask('t1', DateTime.utc(2026, 9, 28)), // this week (Mon) makeTask('t2', DateTime.utc(2026, 9, 29)), // this week (Tue) makeTask('t3', DateTime.utc(2026, 9, 27)), // last week (Sun Sep 27) makeTask('t4', DateTime.utc(2026, 9, 21)), // last week (Mon Sep 21) ]; final stats = computeProgrammerStats( userId: 'user1', now: now, tasks: tasks, sheets: [], leaveDays: {}, todayFocus: Duration.zero, ); expect(stats.completedThisWeek, 2); expect(stats.completedLastWeek, 2); }); group('computeBadges', () { test('tiered badge shows highest earned tier and next target', () { final b = computeBadges(statsWith(completedTotal: 12), includeSheetBadges: false) .firstWhere((b) => b.id == 'finisher'); expect(b.earned, isTrue); expect(b.title, 'Finisher ยท 10'); expect(b.target, 50); }); test('includeSheetBadges false hides sheet-based badges', () { final ids = computeBadges(statsWith(), includeSheetBadges: false) .map((b) => b.id) .toSet(); expect( ids, isNot(containsAll( ['deep_work', 'on_a_roll', 'team_player', 'clean_week']))); }); }); }