import 'dart:async'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/servicios/servicio_alarmas_android.dart'; import 'package:pluriwave/servicios/servicio_grabacion_radio.dart'; /// Shared fake of the Android alarm bridge for alarm-related tests. class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid { final programadas = []; final canceladas = []; final detenidas = []; final ocultadas = []; final soloOcultadas = []; final ejecucionesNativas = []; final snoozesNativos = []; final _eventos = StreamController.broadcast(); bool ignoraOptimizacionBateria = true; int solicitudesExencionBateria = 0; int aperturasConfiguracionNotificaciones = 0; /// Extra diagnostico() fields (fix/alarmas-fiabilidad diagnostics screen). /// Default values mirror the previous hardcoded literals in [diagnostico], /// so every existing test that never sets these keeps seeing the exact /// same snapshot as before. bool puedeProgramarExactas = true; bool notificacionesPermitidas = true; bool puedeUsarPantallaCompleta = true; int alarmasNativasPendientes = 0; String fabricante = 'test'; int versionSdk = 35; /// Test-only failure switch (diagnostics screen, "intent not resolving" /// coverage): when true, every `abrir*`/`solicitar*` system-screen action /// below reports failure (as a real device does when a ROM lacks that /// settings screen), while still recording the attempt via its counter. bool fallaAccionSistema = false; /// Test-only failure switch (Design D7): when true, [programar] throws /// instead of scheduling, enabling failure-path coverage that the fake /// could not otherwise produce. bool fallaProgramar = false; /// Test-only failure switch: when true, [detenerSonidoActivo] reports an /// unconfirmed/failed stop instead of a confirmed one. bool fallaDetener = false; /// Simulates [PluriWaveAlarmService.activeRingingId]: the id the fake /// reports as currently ringing, or null if nothing rings. String? alarmaSonandoIdValor; /// Test-only failure switch (Finding 2, fail-toward-silence): when true, /// [alarmaSonandoId] throws instead of returning a value, exercising the /// [detenerSonidoNativo] fallback in `EstadoAlarmas._detenerSiEstaSonando`. bool fallaConsultaSonando = false; /// Every alarm id [detenerSonidoActivo] was invoked for, in call order. final detencionesActivas = []; /// Test-only gate (RES-2 guard test): when set, [detenerSonidoActivo] /// awaits it before resolving, letting tests exercise an overlapping /// in-flight call. Completer? detenerActivoGate; /// Simulates a native -> Flutter `alarmFired` MethodChannel event. void emitirEvento(EventoAlarmaAndroid evento) => _eventos.add(evento); @override Stream get eventosAlarma => _eventos.stream; @override void configurarLocalizaciones(AppLocalizations l10n) {} @override Future programar(AlarmaMusical alarma) async { if (fallaProgramar) { throw StateError('fake programar failure'); } programadas.add(alarma); } @override Future cancelar(String alarmaId) async { canceladas.add(alarmaId); } @override Future detenerSonidoNativo(String alarmaId) async { detenidas.add(alarmaId); } @override Future alarmaSonandoId() async { if (fallaConsultaSonando) { throw StateError('fake alarmaSonandoId failure'); } return alarmaSonandoIdValor; } @override Future detenerSonidoActivo() async { detencionesActivas.add(alarmaSonandoIdValor ?? ''); final gate = detenerActivoGate; if (gate != null) await gate.future; if (fallaDetener) { return const ResultadoDetencion(detenido: false, estabaSonando: true); } return ResultadoDetencion( detenido: true, estabaSonando: alarmaSonandoIdValor != null, alarmaId: alarmaSonandoIdValor, ); } @override Future ocultarNotificacionAlarma(String alarmaId) async { ocultadas.add(alarmaId); } @override Future ocultarSoloNotificacion(String alarmaId) async { soloOcultadas.add(alarmaId); } @override Future diagnostico() async => DiagnosticoAlarmasAndroid( puedeProgramarExactas: puedeProgramarExactas, notificacionesPermitidas: notificacionesPermitidas, puedeUsarPantallaCompleta: puedeUsarPantallaCompleta, ignoraOptimizacionBateria: ignoraOptimizacionBateria, alarmasNativasPendientes: alarmasNativasPendientes, fabricante: fabricante, versionSdk: versionSdk, ); @override Future solicitarExencionBateria() async { solicitudesExencionBateria++; return !fallaAccionSistema; } @override Future abrirConfiguracionNotificaciones() async { aperturasConfiguracionNotificaciones++; return !fallaAccionSistema; } @override Future obtenerEventoInicial() async => null; @override Future> obtenerEjecucionesNativasGestionadas() async => ejecucionesNativas; @override Future> obtenerEstadoSnoozeNativo() async => List.of(snoozesNativos); int solicitudesPermisoAlarmasExactas = 0; int solicitudesPermisoPantallaCompleta = 0; @override Future solicitarPermisoAlarmasExactas() async { solicitudesPermisoAlarmasExactas++; return !fallaAccionSistema; } @override Future solicitarPermisoNotificaciones() async => true; @override Future solicitarPermisoPantallaCompleta() async { solicitudesPermisoPantallaCompleta++; return !fallaAccionSistema; } Future dispose() => _eventos.close(); } /// Inactive recording service fake, safe for widget tests. class FakeServicioGrabacionRadioInactiva extends ServicioGrabacionRadio { final _controller = StreamController.broadcast(); @override EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva(); @override Stream get estadoStream => _controller.stream; @override Future inicializar() async {} @override Future dispose() => _controller.close(); }