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:shared_preferences/shared_preferences.dart'; import '../helpers/fakes_alarmas.dart'; /// Reported twice on-device: "Posponer" on the pre-notice notification left /// the alarm snoozed 1400+ minutes — a whole day — instead of the configured /// few minutes. /// /// The first fix (7054a4c) guarded the NATIVE anchor, and it was not enough, /// because Dart runs AFTERWARDS on this path: the receiver's `postponeNext` /// fires, then `startActivity`, then `app.dart` dispatches here, and this /// method persists and reschedules. Whatever Dart computes is the value that /// survives. It was the last snooze path with no occurrence guard at all. /// /// It also cannot simply reuse `_ocurrenciaSonando`: the pre-notice's /// occurrence legitimately has NOT arrived yet (the reminder is armed 30 min /// ahead), so the ringing-screen guard would reject a perfectly good anchor. void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); AlarmaMusical diaria(String id) => AlarmaMusical( id: id, nombre: 'Mañana', hora: 16, minuto: 20, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], snoozeMinutos: 5, ); ({EstadoAlarmas estado, FakePuertoAlarmasAndroid android}) montar( DateTime Function() reloj, ) { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( esPremium: () => true, servicio: ServicioAlarmas(reloj: reloj), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); return (estado: estado, android: android); } test('una ocurrencia del DÍA SIGUIENTE se rechaza: pospone minutos, ' 'no 24 horas', () async { // The exact reported shape. The pre-notice for today's 16:20 is on // screen at 16:16; the anchor handed in points at TOMORROW (either the // native spec was already advanced, or app.dart fell back to a // proximaEjecucion that had moved on). var ahora = DateTime(2026, 8, 3, 16, 0); final m = montar(() => ahora); await m.estado.guardarAlarma(diaria('p1')); ahora = DateTime(2026, 8, 3, 16, 16); await m.estado.posponerProximaDesdePreaviso( m.estado.alarmas.single, 5, DateTime(2026, 8, 4, 16, 20), // <- tomorrow ); final snooze = m.estado.alarmas.single.snoozeHasta!; final minutos = snooze.difference(ahora).inMinutes; expect( minutos, lessThan(60), reason: 'la alarma quedó a $minutos min ($snooze). El reporte fue "más de ' '1400 minutos"; cualquier cosa por encima de una hora es el mismo bug', ); expect( m.estado.alarmas.single.snoozeOrigen, isNot(DateTime(2026, 8, 4, 16, 20)), reason: 'el ancla sin validar también se guarda como snoozeOrigen y como ' 'ultimaEjecucionGestionada — envenenaría el estado que a9da855 y ' '0430059 existen para mantener limpio', ); }); test('la ocurrencia REAL del preaviso se respeta aunque esté en el futuro: ' 'ancla en la ocurrencia + N, no en ahora + N', () async { // The whole reason this path needs its own guard instead of reusing // _ocurrenciaSonando: 30 minutes ahead is legitimate here. var ahora = DateTime(2026, 8, 3, 16, 0); final m = montar(() => ahora); await m.estado.guardarAlarma(diaria('p2')); // Pre-notice fires at 15:50; the user taps at 15:52, 28 min before. ahora = DateTime(2026, 8, 3, 15, 52); await m.estado.posponerProximaDesdePreaviso( m.estado.alarmas.single, 5, DateTime(2026, 8, 3, 16, 20), ); expect(m.estado.alarmas.single.snoozeHasta, DateTime(2026, 8, 3, 16, 25)); expect(m.estado.alarmas.single.snoozeOrigen, DateTime(2026, 8, 3, 16, 20)); }); test('un snooze ya envenenado en disco se cura al recalcular', () async { // Devices that ran the buggy build carry snoozeHasta = tomorrow in // SharedPreferences. Without healing it, the alarm keeps reporting // tomorrow on every tick and the user sees no change after updating. final ahora = DateTime(2026, 8, 3, 16, 0); final servicio = ServicioAlarmas(reloj: () => ahora); await servicio.guardarAlarma( diaria('p4').copyWith( snoozeHasta: DateTime(2026, 8, 4, 16, 25), snoozeOrigen: DateTime(2026, 8, 4, 16, 20), ), ); final alarma = (await servicio.recalcularTodas()).alarmas.single; expect(alarma.snoozeHasta, isNull); expect(alarma.proximaProgramable, DateTime(2026, 8, 3, 16, 20)); }); test('un snooze legítimo de 2 horas NO se toca', () async { // posponerEjecucion clamps to 120 minutes, so the ceiling has to sit // above that or the heal would eat real snoozes. final ahora = DateTime(2026, 8, 3, 16, 0); final servicio = ServicioAlarmas(reloj: () => ahora); final hasta = DateTime(2026, 8, 3, 18, 0); await servicio.guardarAlarma( diaria('p5').copyWith(snoozeHasta: hasta, snoozeOrigen: ahora), ); expect( (await servicio.recalcularTodas()).alarmas.single.snoozeHasta, hasta, ); }); test('un ancla absurdamente lejana cae a la ocurrencia propia de la alarma, ' 'no a un valor inventado', () async { var ahora = DateTime(2026, 8, 3, 16, 0); final m = montar(() => ahora); await m.estado.guardarAlarma(diaria('p3')); ahora = DateTime(2026, 8, 3, 16, 10); await m.estado.posponerProximaDesdePreaviso( m.estado.alarmas.single, 5, DateTime(2027, 1, 1, 16, 20), // absurd ); // proximaEjecucion (today 16:20) is inside the pre-notice window, so it // is the right fallback and the snooze lands on 16:25. expect(m.estado.alarmas.single.snoozeHasta, DateTime(2026, 8, 3, 16, 25)); }); }