import 'package:flutter_test/flutter_test.dart'; import 'package:tasq/models/programmer_day_sheet.model.dart'; import 'package:tasq/models/programmer_day_sheet_event.model.dart'; import 'package:tasq/providers/programmer_day_sheets_provider.dart'; // --------------------------------------------------------------------------- // Fake push recorder // --------------------------------------------------------------------------- class _PushCall { final List userIds; final String title; final String body; final Map? data; _PushCall({ required this.userIds, required this.title, required this.body, this.data, }); } class _RecordingPush { final calls = <_PushCall>[]; Future call({ required List userIds, required String title, required String body, Map? data, }) async { calls.add(_PushCall(userIds: userIds, title: title, body: body, data: data)); } } // --------------------------------------------------------------------------- // Fake Supabase-like client — only exposes rpc() // --------------------------------------------------------------------------- class _RpcCall { final String name; final Map? params; _RpcCall(this.name, this.params); } class _FakeClient { final calls = <_RpcCall>[]; Map? _nextResult; Object? _nextError; void setResult(Map result) => _nextResult = result; void setError(Object error) => _nextError = error; Future rpc(String name, {Map? params}) async { calls.add(_RpcCall(name, params)); if (_nextError != null) { final err = _nextError!; _nextError = null; throw err; } final result = _nextResult; _nextResult = null; return result; } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { group('DaySheetsController', () { late _FakeClient fake; late _RecordingPush push; late DaySheetsController controller; setUp(() { fake = _FakeClient(); push = _RecordingPush(); controller = DaySheetsController(fake, push: push.call); }); test('submit calls submit_day_sheet with p_date and pushes admins', () async { fake.setResult({ 'sheet_id': 's1', 'work_date': '2026-09-28', 'programmer_name': 'Paola Ross', 'notify_user_ids': ['adm'], }); final sheetId = await controller.submit(DateTime.utc(2026, 9, 28)); expect(sheetId, 's1'); expect(fake.calls.single.name, 'submit_day_sheet'); expect(fake.calls.single.params, {'p_date': '2026-09-28'}); expect(push.calls.single.title, 'Day sheet submitted'); expect( push.calls.single.body, 'Paola Ross submitted their day sheet for Sep 28, 2026', ); }); test('blank remarks fail fast without an rpc call', () async { await expectLater( controller.disapprove( sheetId: 'sh1', remarks: ' ', flaggedTaskIds: [], ), throwsA( isA().having( (e) => e.code, 'code', DaySheetError.remarksRequired, ), ), ); expect(fake.calls, isEmpty); }); test('already_reviewed maps to a friendly message', () async { fake.setError(Exception('already_reviewed')); DaySheetActionException? caught; try { await controller.submit(DateTime.utc(2026, 9, 28)); } on DaySheetActionException catch (e) { caught = e; } expect(caught, isNotNull); expect(caught!.code, DaySheetError.alreadyReviewed); expect(caught.message, 'This sheet was already reviewed.'); }); test('approve reports approved and skipped ids and pushes per approved sheet', () async { fake.setResult({ 'approved': [ { 'sheet_id': 'sh1', 'work_date': '2026-09-28', 'programmer_id': 'prog1', }, { 'sheet_id': 'sh2', 'work_date': '2026-09-27', 'programmer_id': 'prog2', }, ], 'skipped_ids': ['sh3'], }); final snapshot = DaySheetSnapshot(totalSeconds: 3600, rows: []); final items = [ (sheetId: 'sh1', snapshot: snapshot), (sheetId: 'sh2', snapshot: snapshot), (sheetId: 'sh3', snapshot: snapshot), ]; final result = await controller.approve(items); expect(result.approvedIds, ['sh1', 'sh2']); expect(result.skippedIds, ['sh3']); expect(push.calls.length, 2); expect(push.calls[0].title, 'Day sheet approved'); expect( push.calls[0].body, 'Your day sheet for Sep 28, 2026 was approved', ); expect( push.calls[1].body, 'Your day sheet for Sep 27, 2026 was approved', ); }); test('a failing push does not fail the action', () async { fake.setResult({ 'sheet_id': 's1', 'work_date': '2026-09-28', 'programmer_name': 'Paola Ross', 'notify_user_ids': ['adm'], }); final throwingController = DaySheetsController( fake, push: ({ required List userIds, required String title, required String body, Map? data, }) async { throw Exception('push failed'); }, ); // Should succeed even though push throws. final sheetId = await throwingController.submit(DateTime.utc(2026, 9, 28)); expect(sheetId, 's1'); }); }); // ------------------------------------------------------------------------- group('mapDaySheetError', () { test('maps known substrings to the correct error code', () { expect( mapDaySheetError(Exception('not_authorized')), DaySheetError.notAuthorized, ); expect( mapDaySheetError(Exception('already_reviewed')), DaySheetError.alreadyReviewed, ); expect( mapDaySheetError(Exception('already_submitted')), DaySheetError.alreadySubmitted, ); expect( mapDaySheetError(Exception('remarks_required')), DaySheetError.remarksRequired, ); expect( mapDaySheetError(Exception('body_required')), DaySheetError.bodyRequired, ); expect( mapDaySheetError(Exception('work_date_locked')), DaySheetError.workDateLocked, ); expect( mapDaySheetError(Exception('invalid_snapshot')), DaySheetError.invalidSnapshot, ); expect( mapDaySheetError(Exception('something else entirely')), DaySheetError.unknown, ); }); }); // ------------------------------------------------------------------------- group('DaySheetActionException.message', () { test('returns exact strings for every error code', () { expect( DaySheetActionException(DaySheetError.alreadyReviewed).message, 'This sheet was already reviewed.', ); expect( DaySheetActionException(DaySheetError.alreadySubmitted).message, 'This day sheet was already submitted.', ); expect( DaySheetActionException(DaySheetError.notAuthorized).message, 'You are not allowed to do that.', ); expect( DaySheetActionException(DaySheetError.remarksRequired).message, 'Remarks are required to disapprove.', ); expect( DaySheetActionException(DaySheetError.bodyRequired).message, 'Please write a justification.', ); expect( DaySheetActionException(DaySheetError.workDateLocked).message, 'That day is locked — you can only add entries to pending or disapproved days.', ); expect( DaySheetActionException(DaySheetError.invalidSnapshot).message, 'Could not approve: the sheet data was invalid.', ); expect( DaySheetActionException(DaySheetError.unknown).message, 'Something went wrong. Please try again.', ); }); }); // ------------------------------------------------------------------------- group('ProgrammerDaySheetEvent', () { test('fromMap parses all fields correctly', () { final event = ProgrammerDaySheetEvent.fromMap({ 'id': 'ev1', 'sheet_id': 'sh1', 'kind': 'submitted', 'actor_id': 'u1', 'body': 'some text', 'flagged_task_ids': ['t1', 't2'], 'created_at': '2026-09-28T10:00:00.000Z', }); expect(event.id, 'ev1'); expect(event.sheetId, 'sh1'); expect(event.kind, 'submitted'); expect(event.actorId, 'u1'); expect(event.body, 'some text'); expect(event.flaggedTaskIds, ['t1', 't2']); expect(event.createdAt, DateTime.utc(2026, 9, 28, 10, 0, 0)); }); test('fromMap handles null optional fields', () { final event = ProgrammerDaySheetEvent.fromMap({ 'id': 'ev2', 'sheet_id': 'sh2', 'kind': 'approved', 'created_at': '2026-09-28T10:00:00.000Z', }); expect(event.actorId, isNull); expect(event.body, isNull); expect(event.flaggedTaskIds, isEmpty); }); }); }