import 'package:flutter_test/flutter_test.dart'; import 'package:tasq/providers/tasks_provider.dart'; // Lightweight in-memory fake Supabase client used only for this test. class _FakeClient { final Map>> tables = { 'tickets': [], 'tasks': [], 'teams': [], 'team_members': [], 'duty_schedules': [], 'task_assignments': [], 'task_activity_logs': [], 'notifications': [], }; _FakeQuery from(String table) => _FakeQuery(this, table); } class _FakeQuery { final _FakeClient client; final String table; Map? _eq; List? _in; Map? _insertPayload; Map? _updatePayload; _FakeQuery(this.client, this.table); _FakeQuery select([String? _]) => this; _FakeQuery eq(String field, dynamic value) { _eq = {field: value}; return this; } _FakeQuery inFilter(String field, List values) { _in = values; return this; } Future?> maybeSingle() async { final rows = client.tables[table] ?? []; if (_eq != null) { final field = _eq!.keys.first; final value = _eq![field]; Map? found; for (final r in rows) { if (r[field] == value) { found = Map.from(r); break; } } return found; } return rows.isEmpty ? null : Map.from(rows.first); } Future>> execute() async { final rows = client.tables[table] ?? []; if (_in != null) { // return rows where the field (assume first) is in the list return rows .where((r) => _in!.contains(r.values.first)) .map((r) => Map.from(r)) .toList(); } return rows.map((r) => Map.from(r)).toList(); } _FakeQuery insert(Map payload) { _insertPayload = Map.from(payload); return this; } Future> single() async { if (_insertPayload != null) { // emulate DB-generated id final id = 'tsk-${client.tables['tasks']!.length + 1}'; final row = Map.from(_insertPayload!); row['id'] = id; client.tables[table]!.add(row); return Map.from(row); } if (_updatePayload != null && _eq != null) { final field = _eq!.keys.first; final value = _eq![field]; final idx = client.tables[table]!.indexWhere((r) => r[field] == value); if (idx >= 0) { client.tables[table]![idx] = { ...client.tables[table]![idx], ..._updatePayload!, }; return Map.from(client.tables[table]![idx]); } } throw Exception('single() called in unsupported context in fake'); } Future update(Map payload) async { _updatePayload = payload; if (_eq != null) { final field = _eq!.keys.first; final value = _eq![field]; final idx = client.tables[table]!.indexWhere((r) => r[field] == value); if (idx >= 0) { client.tables[table]![idx] = { ...client.tables[table]![idx], ...payload, }; } } } Future delete() async { if (_eq != null) { final field = _eq!.keys.first; final value = _eq![field]; client.tables[table]!.removeWhere((r) => r[field] == value); } } } void main() { test( 'ticket promotion creates task and auto-assigns late-comer', () async { final fake = _FakeClient(); // ticket row fake.tables['tickets']!.add({ 'id': 'TCK-1', 'subject': 'Printer down', 'description': 'Paper jam', 'office_id': 'office-1', }); // team covering office-1 fake.tables['teams']!.add({ 'id': 'team-1', 'office_ids': ['office-1'], }); // two team members fake.tables['team_members']!.add({'team_id': 'team-1', 'user_id': 'u1'}); fake.tables['team_members']!.add({'team_id': 'team-1', 'user_id': 'u2'}); // both checked in today; u2 arrived later -> should be chosen fake.tables['duty_schedules']!.add({ 'user_id': 'u1', 'check_in_at': DateTime(2026, 2, 18, 8, 0).toIso8601String(), }); fake.tables['duty_schedules']!.add({ 'user_id': 'u2', 'check_in_at': DateTime(2026, 2, 18, 9, 0).toIso8601String(), }); // No existing tasks for ticket // We'll reuse production controllers but point them at our fake client // by creating minimal adapter wrappers. For this test we only need to // exercise selection+assignment via TasksController.createTask. // Create TicketsController-like flow manually (simulate promoted path): // 1) create task row (simulate TasksController.createTask) final taskPayload = { 'title': 'Printer down', 'description': 'Paper jam', 'office_id': 'office-1', 'ticket_id': 'TCK-1', }; // emulate insert -> returns row with id final insertedTask = await fake .from('tasks') .insert(taskPayload) .single(); final taskId = insertedTask['id'] as String; // 2) run simplified autoAssign logic (mirror of production selection) // Gather team ids covering office final teams = fake.tables['teams']!; final teamIds = teams .where((t) => (t['office_ids'] as List).contains('office-1')) .map((t) => t['id'] as String) .toList(); final memberRows = fake.tables['team_members']! .where((m) => teamIds.contains(m['team_id'])) .toList(); final candidateIds = memberRows .map((r) => r['user_id'] as String) .toList(); // read duty_schedules for candidates final onDuty = {}; for (final s in fake.tables['duty_schedules']!) { final uid = s['user_id'] as String; if (!candidateIds.contains(uid)) continue; final checkIn = DateTime.parse(s['check_in_at'] as String); onDuty[uid] = checkIn; } // compute completedToday as 0 (no completed tasks in fake DB) final candidates = onDuty.entries .map( (e) => AutoAssignCandidate( userId: e.key, checkInAt: e.value, completedToday: 0, ), ) .toList(); final chosen = chooseAutoAssignCandidate(candidates); expect(chosen, equals('u2'), reason: 'Late-comer u2 should be chosen'); // Insert assignment row (what _autoAssignTask would do) await fake.from('task_assignments').insert({ 'task_id': taskId, 'user_id': chosen, }).single(); // Assert task exists and assignment was created final createdTask = fake.tables['tasks']!.firstWhere( (t) => t['id'] == taskId, ); expect(createdTask['ticket_id'], equals('TCK-1')); final assignment = fake.tables['task_assignments']!.firstWhere( (a) => a['task_id'] == taskId, orElse: () => {}, ); expect(assignment['user_id'], equals('u2')); }, timeout: Timeout(Duration(seconds: 2)), ); }