import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_alarmas.dart'; import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/servicios/servicio_alarmas.dart'; import 'package:pluriwave/servicios/servicio_alarmas_android.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes_alarmas.dart'; /// Reported on-device: an alarm set for Monday 16:20 never rang, and the /// "next alarm" banner showed a DIFFERENT alarm (the next morning's) instead. /// /// Root cause: `finalizarEjecucion` ("Detener") anchored the completed /// occurrence to `proximaEjecucion` with no check that it was the one /// actually ringing. On the native-fire path the fire-time sync advances /// `proximaEjecucion` to the NEXT occurrence BEFORE the user can reach the /// ring screen — so stopping today's ring recorded NEXT week's occurrence as /// already handled. `ServicioProgramacionAlarmas._esValida` then rejected /// that occurrence for real, and the alarm silently jumped past it: it never /// rang, and every sibling alarm outranked it in the banner. /// /// This is the exact hazard `posponerAlarma` was fixed for in `9c7cf4e` /// ("anchor snooze to the ringing occurrence, never a future one"). The guard /// landed on the snooze path and never on the stop path, which sits directly /// below it in the same file. void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); AlarmaMusical semanalLunes(String id) => AlarmaMusical( id: id, nombre: 'Tarde del lunes', hora: 16, minuto: 20, tipoProgramacion: TipoProgramacionAlarma.diasSemana, diasSemana: const [DateTime.monday], ); test('Detener cierra la ocurrencia que sonaba, no quema la siguiente ' '(el nativo ya avanzó proximaEjecucion antes de que el usuario ' 'llegue a la pantalla)', () async { // Monday 2026-08-03. var ahora = DateTime(2026, 8, 3, 16, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(semanalLunes('a1')); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 8, 3, 16, 20), ); // 16:20 — it rings. The native scheduler records the occurrence handled // and rearms; the cold-start sync brings that over, which advances // proximaEjecucion to NEXT Monday while the alarm is still ringing. ahora = DateTime(2026, 8, 3, 16, 20, 5); android.ejecucionesNativas.add( EjecucionAlarmaNativa( alarmaId: 'a1', gestionadaEn: DateTime(2026, 8, 3, 16, 20), ), ); await estado.inicializar(); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 8, 10, 16, 20), reason: 'precondición: el nativo ya avanzó a la semana siguiente', ); // NOW the user taps "Detener" on the ring screen. await estado.finalizarEjecucion('a1'); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 8, 10, 16, 20), reason: 'Detener debe cerrar la ocurrencia que sonaba (hoy), no consumir ' 'la del lunes que viene empujándola a 2026-08-17', ); expect( estado.alarmas.single.ultimaEjecucionGestionada, isNot(DateTime(2026, 8, 10, 16, 20)), reason: 'marcar como gestionada una ocurrencia futura es justo lo que hace ' 'que _esValida la rechace y esa alarma no suene ese día', ); }); test( 'Detener sin nada sonando tampoco consume la próxima ocurrencia', () async { // Defensive: the ring screen is the only production caller, but a stale // route or a duplicated stop event must not silently eat a day. var ahora = DateTime(2026, 8, 3, 9, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(semanalLunes('a2')); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 8, 3, 16, 20), ); ahora = DateTime(2026, 8, 3, 9, 1); await estado.finalizarEjecucion('a2'); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 8, 3, 16, 20), reason: 'a las 09:01 la ocurrencia de las 16:20 no está sonando', ); }, ); }