import 'dart:async'; 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'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); AlarmaMusical alarmaDiaria(String id, {int snoozeMinutos = 5}) => AlarmaMusical( id: id, nombre: 'Diaria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], snoozeMinutos: snoozeMinutos, ); test('posponerAlarma ancla snoozeHasta en proximaEjecucion + minutos, ' 'programa una sola vez y notifica', () async { final ahora = DateTime(2026, 6, 11, 7, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(alarmaDiaria('s1')); final alarma = estado.alarmas.single; expect(alarma.proximaEjecucion, DateTime(2026, 6, 11, 7, 30)); final programadasAntes = android.programadas.length; var notificaciones = 0; estado.addListener(() => notificaciones++); await estado.posponerAlarma(alarma, 5); expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 6, 11, 7, 35)); expect(estado.alarmas.single.snoozeOrigen, DateTime(2026, 6, 11, 7, 30)); expect(android.programadas.length, programadasAntes + 1); expect(android.programadas.last.snoozeHasta, DateTime(2026, 6, 11, 7, 35)); expect(notificaciones, greaterThanOrEqualTo(1)); }); test( 'la lista de alarmas refleja el snooze de forma sincrona tras posponer', () async { final ahora = DateTime(2026, 6, 11, 7, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(alarmaDiaria('sync1')); final futuro = estado.posponerAlarma(estado.alarmas.single, 10); await futuro; // Sin polls ni esperas adicionales: el estado ya refleja el snooze. expect( estado.alarmas.single.proximaProgramable, DateTime(2026, 6, 11, 7, 40), ); }, ); test( 'evento nativo snoozed registra el snooze sin reprogramar en Android', () async { final ahora = DateTime(2026, 6, 11, 7, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(alarmaDiaria('n1')); final programadasAntes = android.programadas.length; final origen = DateTime(2026, 6, 11, 7, 30); final hasta = DateTime(2026, 6, 11, 7, 40); final notificado = Completer(); estado.addListener(() { if (!notificado.isCompleted) notificado.complete(); }); android.emitirEvento( EventoAlarmaAndroid( alarmaId: 'n1', titulo: 'Diaria', accion: EventoAlarmaAndroid.accionSnoozed, occurrenceAtMillis: origen.millisecondsSinceEpoch, snoozeUntilMillis: hasta.millisecondsSinceEpoch, ), ); await notificado.future; expect(estado.alarmas.single.snoozeHasta, hasta); expect(estado.alarmas.single.snoozeOrigen, origen); expect( android.programadas.length, programadasAntes, reason: 'el nativo ya reprogramo; no debe haber un segundo programar', ); }, ); test( 'recalcularTodas tras posponer preserva snoozeHasta (guard regresion S4)', () async { var ahora = DateTime(2026, 6, 11, 7, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(alarmaDiaria('r1')); await estado.posponerAlarma(estado.alarmas.single, 5); final snooze = estado.alarmas.single.snoozeHasta; expect(snooze, isNotNull); ahora = DateTime(2026, 6, 11, 7, 2); await estado.refrescarProgramacion(); expect(estado.alarmas.single.snoozeHasta, snooze); expect(android.programadas.last.snoozeHasta, snooze); }, ); test( 'inicializar importa snoozes nativos activos sin esperar el poll', () async { final ahora = DateTime.now(); final origen = ahora.subtract(const Duration(minutes: 2)); final hasta = ahora.add(const Duration(minutes: 8)); final servicio = ServicioAlarmas(reloj: () => ahora); final objetivo = ahora.add(const Duration(hours: 1)); await servicio.guardarAlarma( AlarmaMusical( id: 'cold1', nombre: 'Cold start', hora: objetivo.hour, minuto: objetivo.minute, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ), ); final android = FakePuertoAlarmasAndroid() ..snoozesNativos.add( EstadoSnoozeNativo( alarmaId: 'cold1', snoozeHasta: hasta, snoozeOrigen: origen, ), ); final estado = EstadoAlarmas( servicio: servicio, android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.inicializar(); expect(estado.alarmas.single.snoozeHasta, hasta); expect(estado.alarmas.single.snoozeOrigen, origen); expect(android.programadas.last.snoozeHasta, hasta); }, ); test('desactivar una alarma pospuesta limpia el snooze (S2-R5)', () async { final ahora = DateTime(2026, 6, 11, 7, 0); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma(alarmaDiaria('stop1')); await estado.posponerAlarma(estado.alarmas.single, 5); expect(estado.alarmas.single.snoozeHasta, isNotNull); await estado.cambiarActiva(estado.alarmas.single, false); expect(estado.alarmas.single.snoozeHasta, isNull); expect(estado.alarmas.single.activa, isFalse); // El puente real cancela el setAlarmClock para alarmas inactivas. expect(android.programadas.last.activa, isFalse); }); test( 'finalizarEjecucion limpia snooze y reprograma Android sin snooze', () async { final ahora = DateTime(2026, 6, 11, 7, 36); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( AlarmaMusical( id: 'fin1', nombre: 'Diaria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], proximaEjecucion: DateTime(2026, 6, 11, 7, 30), snoozeHasta: DateTime(2026, 6, 11, 7, 40), snoozeOrigen: DateTime(2026, 6, 11, 7, 30), ), ); await estado.finalizarEjecucion('fin1'); expect(estado.alarmas.single.snoozeHasta, isNull); expect(android.ocultadas, contains('fin1')); expect(android.programadas.last.snoozeHasta, isNull); }, ); }