import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/servicios/servicio_alarmas.dart'; import 'package:pluriwave/servicios/servicio_programacion_alarmas.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { group('ServicioProgramacionAlarmas', () { final servicio = ServicioProgramacionAlarmas(); test('calcula la próxima alarma diaria futura', () { final alarma = AlarmaMusical( id: 'a1', nombre: 'Diaria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 5, 21, 7), ); expect(proxima, DateTime(2026, 5, 21, 7, 30)); }); test('salta vacaciones si la alarma no debe sonar esos días', () { final alarma = AlarmaMusical( id: 'a2', nombre: 'Laboral', hora: 8, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], sonarEnVacaciones: false, ); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 8, 1, 7), vacaciones: [ RangoVacaciones( id: 'v1', nombre: 'Verano', inicio: DateTime(2026, 8), fin: DateTime(2026, 8, 3), ), ], ); expect(proxima, DateTime(2026, 8, 4, 8)); }); test('saltar próxima solo omite esa ejecución', () { final alarma = AlarmaMusical( id: 'a3', nombre: 'Diaria', hora: 9, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ); final omitida = DateTime(2026, 5, 22, 9); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 5, 22, 8), excepciones: [ ExcepcionAlarma(alarmaId: 'a3', ejecucion: omitida, tipo: 'skipNext'), ], ); expect(proxima, DateTime(2026, 5, 23, 9)); }); test( 'un registro de fallo de programacion NO omite esa ejecucion (solo ' 'skipNext debe hacerlo)', () { final alarma = AlarmaMusical( id: 'a4', nombre: 'Diaria', hora: 9, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ); final ocurrencia = DateTime(2026, 5, 22, 9); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 5, 22, 8), excepciones: [ ExcepcionAlarma( alarmaId: 'a4', ejecucion: ocurrencia, tipo: ExcepcionAlarma.tipoFalloProgramacion, ), ], ); expect( proxima, ocurrencia, reason: 'un fallo de programacion registrado no debe comportarse ' 'como un salto de usuario', ); }, ); test('snooze solo permite 3, 5 o 10 minutos y cae a 5', () { expect( servicio.calcularSnooze(DateTime(2026, 5, 21, 7), 10), DateTime(2026, 5, 21, 7, 10), ); expect( servicio.calcularSnooze(DateTime(2026, 5, 21, 7), 99), DateTime(2026, 5, 21, 7, 5), ); }); test( 'servicio limpia proxima ejecucion obsoleta al recalcular unica vencida', () async { SharedPreferences.setMockInitialValues({}); final servicioAlarmas = ServicioAlarmas( reloj: () => DateTime(2026, 5, 22, 10), ); final alarma = AlarmaMusical( id: 'a4', nombre: 'Unica vencida', hora: 9, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.unica, diasSemana: const [], fechaUnica: DateTime(2026, 5, 22), proximaEjecucion: DateTime(2026, 5, 22, 9), ); final guardada = await servicioAlarmas.guardarAlarma(alarma); expect(guardada.alarmas.single.proximaEjecucion, isNull); }, ); test('mantiene alarma unica creada dentro del mismo minuto', () { final alarma = AlarmaMusical( id: 'a5', nombre: 'Ahora', hora: 20, minuto: 13, tipoProgramacion: TipoProgramacionAlarma.unica, diasSemana: const [], fechaUnica: DateTime(2026, 5, 23), ); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 5, 23, 20, 13, 45), ); expect(proxima, DateTime(2026, 5, 23, 20, 13, 47)); }); test('calcula siguiente diaria despues de ejecucion completada', () { final alarma = AlarmaMusical( id: 'a6', nombre: 'Diaria', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], proximaEjecucion: DateTime(2026, 5, 25, 7, 30), ); final siguiente = servicio.calcularSiguienteDespuesDeEjecucion( alarma: alarma, ejecucion: DateTime(2026, 5, 25, 7, 30), ); expect(siguiente, DateTime(2026, 5, 26, 7, 30)); }); test('no reutiliza ejecucion gestionada aunque siga en tolerancia', () { final alarma = AlarmaMusical( id: 'a9', nombre: 'Diaria gestionada', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ultimaEjecucionGestionada: DateTime(2026, 5, 25, 7, 30), ); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 5, 25, 7, 30, 20), ); expect(proxima, DateTime(2026, 5, 26, 7, 30)); }); test('la hora de pared nunca deriva al iterar dias (DST-safe): diaria', () { // Property lock for the DST drift bug: advancing candidates with // add(Duration(days: 1)) shifts the ABSOLUTE instant by 86400s, so on // DST-observing machines (this repo's dev machine runs Europe/Madrid) // crossing 2026-03-29 / 2026-10-25 drifted the wall hour +-1h // permanently. Iterating a full year of next-occurrences must keep // every result at exactly hora:minuto. final alarma = AlarmaMusical( id: 'dst-diaria', nombre: 'Diaria DST', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ); var proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 1, 5, 6, 0), )!; for (var i = 0; i < 400; i++) { expect( proxima.hour, 7, reason: 'deriva de hora de pared en ${proxima.toIso8601String()}', ); expect(proxima.minute, 30); proxima = servicio.calcularSiguienteDespuesDeEjecucion( alarma: alarma, ejecucion: proxima, )!; } }); test('la hora de pared nunca deriva al iterar dias (DST-safe): ' 'dias de semana', () { final alarma = AlarmaMusical( id: 'dst-semana', nombre: 'Semana DST', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diasSemana, diasSemana: const [1, 3, 5], ); var proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 1, 5, 6, 0), )!; for (var i = 0; i < 200; i++) { expect( proxima.hour, 7, reason: 'deriva de hora de pared en ${proxima.toIso8601String()}', ); expect(proxima.minute, 30); expect(const [1, 3, 5], contains(proxima.weekday)); proxima = servicio.calcularSiguienteDespuesDeEjecucion( alarma: alarma, ejecucion: proxima, )!; } }); test('mantiene la ocurrencia diaria cuyo disparo acaba de pasar', () { // Contract the native AlarmScheduler.computeNextTriggerMillis MUST // mirror: a trigger that just passed within toleranciaDisparoInminente // (90s) and has NOT been handled is kept (normalized to desde + 2s), // never skipped to tomorrow. The native side lacking this window is // what recomputed the next occurrence as tomorrow and cancelled the // in-flight fire, so the alarm never rang until the screen was turned // on. If this expectation changes, the native constant // IMMINENT_TOLERANCE_MILLIS must change with it. final alarma = AlarmaMusical( id: 'a-inminente-diaria', nombre: 'Diaria inminente', hora: 22, minuto: 35, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], ); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 7, 11, 22, 35, 30), ); expect(proxima, DateTime(2026, 7, 11, 22, 35, 32)); }); test('mantiene la ocurrencia por dias de semana recien pasada', () { final alarma = AlarmaMusical( id: 'a-inminente-semana', nombre: 'Semana inminente', hora: 22, minuto: 35, tipoProgramacion: TipoProgramacionAlarma.diasSemana, diasSemana: const [1, 2, 3, 4, 5, 6, 7], ); final proxima = servicio.calcularProxima( alarma: alarma, desde: DateTime(2026, 7, 11, 22, 35, 30), ); expect(proxima, DateTime(2026, 7, 11, 22, 35, 32)); }); test('calcula siguiente por dias de semana despues de ejecucion', () { final alarma = AlarmaMusical( id: 'a7', nombre: 'Laboral', hora: 8, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diasSemana, diasSemana: const [DateTime.monday, DateTime.wednesday], proximaEjecucion: DateTime(2026, 5, 25, 8), ); final siguiente = servicio.calcularSiguienteDespuesDeEjecucion( alarma: alarma, ejecucion: DateTime(2026, 5, 25, 8), ); expect(siguiente, DateTime(2026, 5, 27, 8)); }); test('alarma unica completada no calcula siguiente', () { final alarma = AlarmaMusical( id: 'a8', nombre: 'Unica', hora: 8, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.unica, diasSemana: const [], fechaUnica: DateTime(2026, 5, 25), proximaEjecucion: DateTime(2026, 5, 25, 8), ); final siguiente = servicio.calcularSiguienteDespuesDeEjecucion( alarma: alarma, ejecucion: DateTime(2026, 5, 25, 8), ); expect(siguiente, isNull); }); }); }