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 fallosProgramacionNativos = []; 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; String fabricante = 'test'; int versionSdk = 35; /// Ids [programar] most recently scheduled as active-with-a-next-run (kept /// in sync with [cancelar] too), mirroring the real native scheduler's own /// pending-alarm registry (fix/alarmas-fallos-silenciosos, item 3: "verify /// the alarm is actually registered"). Backs [alarmasNativasPendientes]'s /// DEFAULT so a test that never touches that field gets a value that /// tracks reality instead of a frozen `0` -- a test that explicitly /// assigns the field (many `pantalla_diagnostico_alarmas_test.dart` cases /// do, to model a stale/corrupt native count on purpose) keeps getting /// EXACTLY that value regardless of what programar/cancelar do afterward. final _idsRegistradosNativamente = {}; int? _alarmasNativasPendientesFijado; int get alarmasNativasPendientes => _alarmasNativasPendientesFijado ?? _idsRegistradosNativamente.length; set alarmasNativasPendientes(int valor) => _alarmasNativasPendientesFijado = valor; /// 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 PER-ALARM failure switch (fix/alarmas-fallos-silenciosos): /// [programar] throws only for ids in this set, letting a test simulate /// one alarm failing to schedule while its siblings succeed -- the global /// [fallaProgramar] switch cannot express that (it fails everything). final Set idsFallanProgramar = {}; /// 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 || idsFallanProgramar.contains(alarma.id)) { throw StateError('fake programar failure'); } programadas.add(alarma); if (alarma.activa && alarma.proximaProgramable != null) { _idsRegistradosNativamente.add(alarma.id); } else { _idsRegistradosNativamente.remove(alarma.id); } } @override Future cancelar(String alarmaId) async { canceladas.add(alarmaId); _idsRegistradosNativamente.remove(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); @override Future> obtenerFallosProgramacionNativos() async => List.of(fallosProgramacionNativos); int solicitudesPermisoAlarmasExactas = 0; int solicitudesPermisoPantallaCompleta = 0; /// Native-recorded failures the next read should return. Tests seed this /// to simulate a pre-notice that never armed, a refused foreground-service /// start, or a per-alarm reschedule that failed after a reboot. List fallosNativos = const []; int lecturasFallosNativos = 0; /// Simulates an older native build with no such channel method. bool fallaLecturaFallosNativos = false; @override Future> fallosNativosProgramacion() async { lecturasFallosNativos++; if (fallaLecturaFallosNativos) { throw StateError('canal no disponible'); } return fallosNativos; } @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(); }