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({}); }); test('posponer persiste snooze y refrescarProgramacion no lo pisa', () async { var ahora = DateTime(2026, 5, 25, 7, 31); final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'a1', nombre: 'Diaria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); final alarma = estado.alarmas.single; await estado.posponerAlarma(alarma, 5); // Ancla unificada (S2-R6): proximaEjecucion (7:31:02, normalizada por // inminencia) + 5 minutos — ya no "ahora + 5". expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 5, 25, 7, 36, 2)); expect( android.programadas.last.proximaProgramable, DateTime(2026, 5, 25, 7, 36, 2), ); ahora = DateTime(2026, 5, 25, 7, 32); await estado.refrescarProgramacion(); expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 5, 25, 7, 36, 2)); expect( android.programadas.last.proximaProgramable, DateTime(2026, 5, 25, 7, 36, 2), ); }); test( 'posponer desde preaviso mueve esta ejecucion desde la hora original', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( AlarmaMusical( id: 'pre1', nombre: 'Preaviso', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], proximaEjecucion: DateTime(2026, 5, 25, 7, 30), ), ); final alarma = estado.alarmas.single; await estado.posponerProximaDesdePreaviso( alarma, 10, DateTime(2026, 5, 25, 7, 30), ); expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 5, 25, 7, 40)); expect(estado.alarmas.single.snoozeOrigen, DateTime(2026, 5, 25, 7, 30)); expect( android.programadas.last.proximaProgramable, DateTime(2026, 5, 25, 7, 40), ); }, ); test('finalizar diaria calcula siguiente dia y limpia snooze', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( AlarmaMusical( id: 'a2', nombre: 'Diaria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], proximaEjecucion: DateTime(2026, 5, 25, 7, 30), snoozeHasta: DateTime(2026, 5, 25, 7, 36), snoozeOrigen: DateTime(2026, 5, 25, 7, 30), ), ); await estado.finalizarEjecucion('a2'); expect(estado.alarmas.single.snoozeHasta, isNull); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 5, 26, 7, 30), ); }); test('finalizar unica la desactiva y queda sin proxima ejecucion', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( AlarmaMusical( id: 'a3', nombre: 'Unica', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.unica, diasSemana: const [], fechaUnica: DateTime(2026, 5, 25), proximaEjecucion: DateTime(2026, 5, 25, 7, 30), ), ); await estado.finalizarEjecucion('a3'); expect(estado.alarmas.single.activa, isFalse); expect(estado.alarmas.single.proximaEjecucion, isNull); }); test( 'solicita exencion de bateria una sola vez cuando no esta exenta', () async { final android = FakePuertoAlarmasAndroid()..ignoraOptimizacionBateria = false; final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'bat1', nombre: 'Bateria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); expect(android.solicitudesExencionBateria, 1); await estado.guardarAlarma( const AlarmaMusical( id: 'bat2', nombre: 'Bateria 2', hora: 8, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); expect(android.solicitudesExencionBateria, 1); }, ); test('no solicita exencion de bateria cuando ya esta exenta', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'bat3', nombre: 'Exenta', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); expect(android.solicitudesExencionBateria, 0); }); test( 'cambiarActiva(false) detiene el audio cuando la alarma esta sonando (SS-1a)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'ring1', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.alarmaSonandoIdValor = 'ring1'; await estado.cambiarActiva(estado.alarmas.single, false); expect(android.detencionesActivas, contains('ring1')); }, ); test( 'guardarAlarma detiene el audio cuando edita la alarma sonando (SS-1b)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'ring2', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.alarmaSonandoIdValor = 'ring2'; await estado.guardarAlarma(estado.alarmas.single.copyWith(minuto: 45)); expect(android.detencionesActivas, contains('ring2')); }, ); test( 'eliminarAlarma detiene el audio antes de cancelar cuando esta sonando ' '(SS-1c, guardia de regresion)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'ring3', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.alarmaSonandoIdValor = 'ring3'; await estado.eliminarAlarma('ring3'); expect(android.detencionesActivas, contains('ring3')); expect(android.canceladas, contains('ring3')); }, ); test( 'eliminarAlarma usa detenerSonidoNativo cuando la consulta de sonando ' 'falla (fail-toward-silence, regresion de eliminarAlarma)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'ring4', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.fallaConsultaSonando = true; await estado.eliminarAlarma('ring4'); expect(android.detenidas, contains('ring4')); expect(android.canceladas, contains('ring4')); }, ); test( 'guardarAlarma (deshabilitar) usa detenerSonidoNativo cuando la consulta ' 'de sonando falla (fail-toward-silence)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'ring5', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.fallaConsultaSonando = true; await estado.cambiarActiva(estado.alarmas.single, false); expect(android.detenidas, contains('ring5')); }, ); test( 'mutar una alarma distinta a la que suena no dispara el guard (SS-1d)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'y1', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); await estado.guardarAlarma( const AlarmaMusical( id: 'z1', nombre: 'Quieta', hora: 8, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.alarmaSonandoIdValor = 'y1'; final z1 = estado.alarmas.firstWhere((a) => a.id == 'z1'); await estado.cambiarActiva(z1, false); expect(android.detencionesActivas, isEmpty); }, ); test( 'finalizarEjecucion no registra error cuando el stop nativo se confirma ' '(SS-2a)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'fin1', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); android.alarmaSonandoIdValor = 'fin1'; await estado.finalizarEjecucion('fin1'); expect(android.detencionesActivas, contains('fin1')); expect(estado.error, isNull); }, ); test( 'finalizarEjecucion registra error cuando el stop nativo no se confirma ' '(SS-2b)', () async { final android = FakePuertoAlarmasAndroid()..fallaDetener = true; final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'fin2', nombre: 'Sonando', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); await estado.finalizarEjecucion('fin2'); expect(estado.error, isNotNull); }, ); test( 'forzarDetencion reintenta el stop nativo y limpia el error si tiene ' 'exito (SS-3b)', () async { final android = FakePuertoAlarmasAndroid()..fallaDetener = true; final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'force1', nombre: 'Forzada', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); await estado.finalizarEjecucion('force1'); expect(estado.error, isNotNull); android.fallaDetener = false; await estado.forzarDetencion('force1'); expect(estado.error, isNull); expect(android.detencionesActivas.length, 2); }, ); test( 'forzarDetencion mantiene el error si el reintento tambien falla (SS-3b)', () async { final android = FakePuertoAlarmasAndroid()..fallaDetener = true; final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( const AlarmaMusical( id: 'force2', nombre: 'Forzada', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); await estado.finalizarEjecucion('force2'); await estado.forzarDetencion('force2'); expect(estado.error, isNotNull); }, ); test( 'evento nativo missed completa la ejecucion (Phase 6)', () async { final android = FakePuertoAlarmasAndroid(); final estado = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)), android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.guardarAlarma( AlarmaMusical( id: 'miss1', nombre: 'Perdida', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], proximaEjecucion: DateTime(2026, 5, 25, 7, 30), ), ); final notificado = Completer(); estado.addListener(() { if (!notificado.isCompleted) notificado.complete(); }); android.emitirEvento( EventoAlarmaAndroid( alarmaId: 'miss1', titulo: 'Perdida', accion: EventoAlarmaAndroid.accionMissed, occurrenceAtMillis: DateTime(2026, 5, 25, 7, 30).millisecondsSinceEpoch, ), ); await notificado.future; expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 5, 26, 7, 30), ); }, ); test( 'inicializar sincroniza ejecucion nativa y evita reprogramar al instante', () async { final android = FakePuertoAlarmasAndroid() ..ejecucionesNativas.add( EjecucionAlarmaNativa( alarmaId: 'native1', gestionadaEn: DateTime(2026, 5, 25, 7, 30), ), ); final servicio = ServicioAlarmas( reloj: () => DateTime(2026, 5, 25, 7, 30, 20), ); await servicio.guardarAlarma( AlarmaMusical( id: 'native1', nombre: 'Nativa', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], proximaEjecucion: DateTime(2026, 5, 25, 7, 30), ), ); final estado = EstadoAlarmas( servicio: servicio, android: android, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); addTearDown(android.dispose); await estado.inicializar(); expect( estado.alarmas.single.ultimaEjecucionGestionada, DateTime(2026, 5, 25, 7, 30), ); expect( estado.alarmas.single.proximaEjecucion, DateTime(2026, 5, 26, 7, 30), ); expect( android.programadas.last.proximaProgramable, DateTime(2026, 5, 26, 7, 30), ); }, ); }