import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_alarmas.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/pantallas/pantalla_alarmas.dart'; import 'package:pluriwave/servicios/servicio_alarmas.dart'; import 'package:pluriwave/widgets/pluri_push_scaffold.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes.dart'; import '../helpers/fakes_alarmas.dart'; class _Entorno { _Entorno({required this.estadoAlarmas}); final EstadoAlarmas estadoAlarmas; } class _EntornoDosAlarmas { _EntornoDosAlarmas({required this.estadoAlarmas, required this.android}); final EstadoAlarmas estadoAlarmas; final FakePuertoAlarmasAndroid android; } /// WU8 fixture: two DAILY alarms ('r1' 07:00, 'r2' 08:00) under a FIXED clock /// (06:00, before both) so 'r1' is deterministically the featured/soonest /// alarm regardless of the wall-clock time the suite happens to run at. Future<_EntornoDosAlarmas> _montarDosAlarmas(WidgetTester tester) async { tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final radio = EstadoRadio( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), iniciarAutomaticamente: false, ); addTearDown(radio.dispose); final android = FakePuertoAlarmasAndroid(); final estadoAlarmas = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)), android: android, iniciarAutomaticamente: false, ); addTearDown(estadoAlarmas.dispose); addTearDown(android.dispose); await estadoAlarmas.guardarAlarma( const AlarmaMusical( id: 'r1', nombre: 'Primera', hora: 7, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); await estadoAlarmas.guardarAlarma( const AlarmaMusical( id: 'r2', nombre: 'Segunda', hora: 8, minuto: 0, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], ), ); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: radio), ChangeNotifierProvider.value(value: estadoAlarmas), ], child: MaterialApp( locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: PantallaAlarmas()), ), ), ); await tester.pumpAndSettle(); return _EntornoDosAlarmas(estadoAlarmas: estadoAlarmas, android: android); } Future<_Entorno> _abrirEditor(WidgetTester tester) async { tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final favoritos = FakeServicioFavoritos(); await favoritos.agregar(emisoraDemo(uuid: 'alfa', nombre: 'Alfa FM')); await favoritos.agregar(emisoraDemo(uuid: 'beta', nombre: 'Beta FM')); final radio = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), iniciarAutomaticamente: false, ); addTearDown(radio.dispose); await radio.cargarFavoritos(); final android = FakePuertoAlarmasAndroid(); final estadoAlarmas = EstadoAlarmas( servicio: ServicioAlarmas(reloj: DateTime.now), android: android, iniciarAutomaticamente: false, ); addTearDown(estadoAlarmas.dispose); addTearDown(android.dispose); await estadoAlarmas.guardarAlarma( const AlarmaMusical( id: 'ed1', nombre: 'Semanal', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diasSemana, diasSemana: [DateTime.monday], ), ); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: radio), ChangeNotifierProvider.value(value: estadoAlarmas), ], child: MaterialApp( locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: PantallaAlarmas()), ), ), ); await tester.pumpAndSettle(); // WU8: the always-visible "Edit" button is gone — tapping the simplified // card itself now opens the editor. final tarjeta = find.byKey(const ValueKey('tarjeta-alarma-ed1')); await tester.ensureVisible(tarjeta); await tester.pumpAndSettle(); await tester.tap(tarjeta); await tester.pumpAndSettle(); return _Entorno(estadoAlarmas: estadoAlarmas); } String _textoPreview(WidgetTester tester) { final texto = tester.widget( find.descendant( of: find.byKey(const ValueKey('next-trigger-preview')), matching: find.byType(Text), ), ); return texto.data ?? ''; } void main() { final l10n = lookupAppLocalizations(const Locale('es')); setUp(() { SharedPreferences.setMockInitialValues({}); }); testWidgets( 'muestra la proxima ejecucion y la actualiza al cambiar la recurrencia ' '(S2-R8)', (tester) async { await _abrirEditor(tester); expect( find.byKey(const ValueKey('next-trigger-preview')), findsOneWidget, ); final antes = _textoPreview(tester); expect(antes, isNot(l10n.alarmNoNextExecution)); // Lunes -> Martes: la fecha calculada SIEMPRE cambia, sea cual sea hoy. await tester.tap(find.text(l10n.weekdayShortTuesday)); await tester.pumpAndSettle(); await tester.tap(find.text(l10n.weekdayShortMonday)); await tester.pumpAndSettle(); final despues = _textoPreview(tester); expect(despues, isNot(l10n.alarmNoNextExecution)); expect(despues, isNot(antes)); }, ); testWidgets( 'el selector de emisora abre un bottom sheet con buscador (S2-R9)', (tester) async { await _abrirEditor(tester); await tester.ensureVisible( find.byKey(const ValueKey('alarm-station-field')), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('alarm-station-field'))); await tester.pumpAndSettle(); expect(find.byType(SearchBar), findsOneWidget); final lista = find.byType(ListView).last; expect( find.descendant(of: lista, matching: find.text('Alfa FM')), findsOneWidget, ); expect( find.descendant(of: lista, matching: find.text('Beta FM')), findsOneWidget, ); await tester.enterText(find.byType(TextField).last, 'beta'); await tester.pumpAndSettle(); expect( find.descendant(of: lista, matching: find.text('Alfa FM')), findsNothing, ); expect( find.descendant(of: lista, matching: find.text('Beta FM')), findsOneWidget, ); }, ); testWidgets( 'tambien existe un selector para la emisora de respaldo (S2-R9)', (tester) async { await _abrirEditor(tester); await tester.ensureVisible( find.byKey(const ValueKey('alarm-fallback-station-field')), ); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('alarm-fallback-station-field')), ); await tester.pumpAndSettle(); expect(find.byType(SearchBar), findsOneWidget); }, ); testWidgets('permite configurar la duracion del snooze (S2-R10)', ( tester, ) async { final entorno = await _abrirEditor(tester); await tester.ensureVisible(find.text(l10n.alarmSnoozeDurationTitle)); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), findsWidgets); await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(10))); await tester.pumpAndSettle(); await tester.ensureVisible(find.text(l10n.saveAlarmAction)); await tester.pumpAndSettle(); await tester.tap(find.text(l10n.saveAlarmAction)); await tester.pumpAndSettle(); expect(entorno.estadoAlarmas.alarmas.single.snoozeMinutos, 10); }); testWidgets('el slider de volumen permite bajar hasta 0.0 (S2-R11)', ( tester, ) async { await _abrirEditor(tester); final sliders = tester.widgetList(find.byType(Slider)); final volumen = sliders.firstWhere((slider) => slider.max == 1.0); expect(volumen.min, 0.0); }); group('WU8 — tarjeta de alarma simplificada', () { testWidgets( 'tocar la tarjeta abre el editor precargado con los datos de esa ' 'alarma', (tester) async { await _montarDosAlarmas(tester); await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2'))); await tester.pumpAndSettle(); expect(find.text(l10n.editAlarmTitle), findsOneWidget); expect(find.text('Segunda'), findsOneWidget); }, ); testWidgets('deslizar la tarjeta pide confirmacion; cancelar la conserva y ' 'confirmar la elimina', (tester) async { final entorno = await _montarDosAlarmas(tester); // Cancelar: la tarjeta se conserva. await tester.drag( find.byKey(const ValueKey('tarjeta-alarma-r2')), const Offset(-600, 0), ); await tester.pumpAndSettle(); expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget); await tester.tap(find.text(l10n.cancelAction)); await tester.pumpAndSettle(); expect(entorno.estadoAlarmas.alarmas.length, 2); expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsOneWidget); // Confirmar: la tarjeta se elimina. await tester.drag( find.byKey(const ValueKey('tarjeta-alarma-r2')), const Offset(-600, 0), ); await tester.pumpAndSettle(); expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget); await tester.tap(find.text(l10n.deleteAction)); await tester.pumpAndSettle(); expect(entorno.estadoAlarmas.alarmas.length, 1); expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsNothing); }); testWidgets( 'el banner destacado permite saltar la alarma mas proxima (accion ' '"Saltar")', (tester) async { final entorno = await _montarDosAlarmas(tester); final proximaAntes = entorno.estadoAlarmas.proximaAlarma; expect(proximaAntes?.id, 'r1'); await tester.tap(find.byKey(const ValueKey('hero-skip-next'))); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); final r1 = entorno.estadoAlarmas.alarmas.firstWhere( (a) => a.id == 'r1', ); expect(r1.proximaEjecucion, isNot(proximaAntes!.proximaEjecucion)); }, ); testWidgets('la fila resumen de vacaciones empuja una pantalla al tocarla ' '(destino provisorio; WU9 lo reemplaza)', (tester) async { await _montarDosAlarmas(tester); await tester.tap(find.byKey(const ValueKey('vacaciones-resumen'))); await tester.pumpAndSettle(); expect(find.byType(PluriPushScaffold), findsOneWidget); }); testWidgets( 'guardia de capacidad: editar (tap), saltar (hero) y eliminar (swipe) ' 'siguen siendo alcanzables tras la simplificacion de la tarjeta', (tester) async { final entorno = await _montarDosAlarmas(tester); // 1) Saltar, desde el banner destacado. await tester.tap(find.byKey(const ValueKey('hero-skip-next'))); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); // 2) Editar, tocando la tarjeta. await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2'))); await tester.pumpAndSettle(); expect(find.text(l10n.editAlarmTitle), findsOneWidget); await tester.tap(find.byIcon(Icons.close_rounded)); await tester.pumpAndSettle(); // 3) Eliminar, deslizando y confirmando. await tester.drag( find.byKey(const ValueKey('tarjeta-alarma-r2')), const Offset(-600, 0), ); await tester.pumpAndSettle(); await tester.tap(find.text(l10n.deleteAction)); await tester.pumpAndSettle(); expect(entorno.estadoAlarmas.alarmas.length, 1); }, ); }); }