import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/servicios/servicio_grabacion_radio.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { group('ServicioGrabacionRadio', () { test( 'guarda el stream original en disco con extensión por codec', () async { final dir = await Directory.systemTemp.createTemp('pluriwave-rec-'); final servicio = ServicioGrabacionRadio( cliente: MockClient((request) async { return http.Response.bytes( [1, 2, 3, 4, 5], 200, headers: {'content-type': 'audio/mpeg'}, ); }), resolverDirectorioBase: () async => dir, reloj: () => DateTime(2026, 5, 21, 18, 30), ); await servicio.iniciar( const Emisora( uuid: 'r1', nombre: 'Radio Prueba', url: 'https://stream.example/radio', codec: 'MP3', ), ); await Future.delayed(Duration.zero); final carpeta = Directory( '${dir.path}${Platform.pathSeparator}grabaciones', ); final archivos = await carpeta.list().where((e) => e is File).toList(); expect(archivos, hasLength(1)); expect(archivos.single.path, endsWith('.mp3')); expect(await File(archivos.single.path).readAsBytes(), [1, 2, 3, 4, 5]); expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.inactiva); expect(servicio.ultimoArchivo?.path, archivos.single.path); await servicio.dispose(); }, ); test('detiene una grabación activa bajo demanda', () async { final dir = await Directory.systemTemp.createTemp('pluriwave-rec-stop-'); final controller = StreamController>(); final servicio = ServicioGrabacionRadio( cliente: _StreamClient(controller.stream), resolverDirectorioBase: () async => dir, ); await servicio.iniciar( const Emisora( uuid: 'r2', nombre: 'Radio Larga', url: 'https://stream.example/live', codec: 'aac', ), ); controller.add([10, 20, 30]); await Future.delayed(Duration.zero); expect(servicio.estado.activa, isTrue); expect(servicio.estado.bytes, 3); await servicio.detener(); expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.inactiva); expect(servicio.ultimoArchivo, isNotNull); await controller.close(); await servicio.dispose(); }); test('se detiene automáticamente al alcanzar el tamaño máximo', () async { SharedPreferences.setMockInitialValues({}); final dir = await Directory.systemTemp.createTemp('pluriwave-rec-max-'); final controller = StreamController>(); final servicio = ServicioGrabacionRadio( cliente: _StreamClient(controller.stream), resolverDirectorioBase: () async => dir, ); await servicio.inicializar(); await servicio.guardarMaxBytes(3); await servicio.iniciar( const Emisora( uuid: 'r3', nombre: 'Radio Corta', url: 'https://stream.example/short', codec: 'mp3', ), ); controller.add([1, 2, 3]); await Future.delayed(Duration.zero); await Future.delayed(const Duration(milliseconds: 20)); expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.inactiva); await controller.close(); await servicio.dispose(); }); // T-S6-05-A: a stream error triggers _fallar, which must clear active state // and set status to error (S7-R5 invariant: no reconnect, immediate clear). test('error de stream llama _fallar: estado pasa a error y no queda activa ' '(T-S6-05-A)', () async { final dir = await Directory.systemTemp.createTemp('pluriwave-rec-err-'); final errorController = StreamController>(); final servicio = ServicioGrabacionRadio( cliente: _StreamClient(errorController.stream), resolverDirectorioBase: () async => dir, ); await servicio.iniciar( const Emisora( uuid: 'r4', nombre: 'Radio Error', url: 'https://stream.example/bad', ), ); // Emit a byte so the stream is in "grabando" state, then error. errorController.add([1]); await Future.delayed(Duration.zero); errorController.addError(Exception('network failure')); await Future.delayed(const Duration(milliseconds: 20)); expect( servicio.estado.activa, isFalse, reason: '_fallar must clear activa flag immediately', ); expect( servicio.estado.tipo, EstadoGrabacionRadioTipo.error, reason: '_fallar sets status to error, not inactiva', ); expect(servicio.estado.error, contains('network failure')); await errorController.close(); await servicio.dispose(); }); // T-S6-05-B: after an error, a subsequent iniciar call must succeed because // _fallar resets all internal state (subscriptions, sink, client). test('tras un error, iniciar de nuevo tiene exito (T-S6-05-B)', () async { final dir = await Directory.systemTemp.createTemp('pluriwave-rec-retry-'); final errorController = StreamController>(); final servicio = ServicioGrabacionRadio( cliente: _StreamClient(errorController.stream), resolverDirectorioBase: () async => dir, ); // First attempt — error. await servicio.iniciar( const Emisora( uuid: 'r5', nombre: 'Radio Retry', url: 'https://stream.example/retry', ), ); errorController.addError(Exception('transient error')); await Future.delayed(const Duration(milliseconds: 20)); await errorController.close(); expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.error); // Second attempt with a clean stream — must not throw StateError. final okController = StreamController>(); final servicio2 = ServicioGrabacionRadio( cliente: _StreamClient(okController.stream), resolverDirectorioBase: () async => dir, ); await expectLater( servicio2.iniciar( const Emisora( uuid: 'r5', nombre: 'Radio Retry', url: 'https://stream.example/retry', ), ), completes, reason: 'after error state, a fresh service iniciar must not throw', ); await okController.close(); await servicio.dispose(); await servicio2.dispose(); }); }); } class _StreamClient extends http.BaseClient { _StreamClient(this.stream); final Stream> stream; @override Future send(http.BaseRequest request) async { return http.StreamedResponse( stream, 200, headers: {'content-type': 'audio/aac'}, ); } }