import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/controlador_reconexion.dart'; import 'package:pluriwave/servicios/servicio_audio.dart'; /// Fake timer injected through the controller's timer factory so backoff /// scheduling is observable without real delays or platform channels. class _TemporizadorFalso implements Timer { _TemporizadorFalso(this.duracion, this.callback); final Duration duracion; final void Function() callback; bool cancelado = false; @override void cancel() => cancelado = true; @override bool get isActive => !cancelado; @override int get tick => 0; void disparar() { if (!cancelado) callback(); } } ControladorReconexion _controlador({ int maxReintentos = 5, Duration retrasoMaximo = const Duration(seconds: 30), required List<_TemporizadorFalso> temporizadores, }) { return ControladorReconexion( maxReintentos: maxReintentos, retrasoMaximo: retrasoMaximo, crearTemporizador: (duracion, callback) { final timer = _TemporizadorFalso(duracion, callback); temporizadores.add(timer); return timer; }, ); } void main() { group('ControladorReconexion — backoff (S7-R2-D, S7-R7)', () { test('secuencia de retrasos 1-5 es [1s, 2s, 4s, 8s, 16s]', () { final controlador = ControladorReconexion(); expect( [for (var i = 1; i <= 5; i++) controlador.retrasoParaIntento(i)], const [ Duration(seconds: 1), Duration(seconds: 2), Duration(seconds: 4), Duration(seconds: 8), Duration(seconds: 16), ], ); }); test('el retraso queda limitado por retrasoMaximo', () { final controlador = ControladorReconexion( retrasoMaximo: Duration(seconds: 10), ); expect( controlador.retrasoParaIntento(5), const Duration(seconds: 10), reason: '16s sin limite, pero el maximo configurado es 10s', ); expect( ControladorReconexion().retrasoParaIntento(6), const Duration(seconds: 30), reason: '32s supera el maximo por defecto de 30s', ); }); }); group('ControladorReconexion — decision de reintento (S7-R2, S7-R7)', () { test('intencion=true + fallo => reintento programado con backoff', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = _controlador(temporizadores: temporizadores); var reintentos = 0; final decision = controlador.registrarFallo( intencionReproducir: true, alReintentar: () => reintentos++, ); expect(decision, DecisionReconexion.reintentar); expect(controlador.intentos, 1); expect(controlador.reintentoPendiente, isTrue); expect(temporizadores, hasLength(1)); expect(temporizadores.single.duracion, const Duration(seconds: 1)); temporizadores.single.disparar(); expect(reintentos, 1, reason: 'el callback corre al vencer el backoff'); }); test('intencion=false + fallo => NO se programa reintento (S7-R2-B)', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = _controlador(temporizadores: temporizadores); final decision = controlador.registrarFallo( intencionReproducir: false, alReintentar: () => fail('no debe reintentar con intencion=false'), ); expect(decision, DecisionReconexion.ignorar); expect(controlador.intentos, 0); expect(controlador.reintentoPendiente, isFalse); expect(temporizadores, isEmpty); }); test('tras maxReintentos agotados => agotado, sin mas reintentos ' '(S7-R2-C)', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = _controlador( maxReintentos: 2, temporizadores: temporizadores, ); for (var i = 0; i < 2; i++) { expect( controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ), DecisionReconexion.reintentar, ); } final decision = controlador.registrarFallo( intencionReproducir: true, alReintentar: () => fail('no debe programar tras agotar reintentos'), ); expect(decision, DecisionReconexion.agotado); expect(controlador.reintentoPendiente, isFalse); expect(temporizadores, hasLength(2)); }); test('una reconexion exitosa restablece el contador (S7-R7)', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = _controlador(temporizadores: temporizadores); controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); expect(controlador.intentos, 2); controlador.restablecer(); expect(controlador.intentos, 0); expect(controlador.reintentoPendiente, isFalse); controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); expect( temporizadores.last.duracion, const Duration(seconds: 1), reason: 'tras restablecer, el backoff arranca de nuevo en la base', ); }); test('stop del usuario durante el stall cancela el reintento (S7-R6)', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = _controlador(temporizadores: temporizadores); var reintentos = 0; controlador.registrarFallo( intencionReproducir: true, alReintentar: () => reintentos++, ); expect(controlador.reintentoPendiente, isTrue); controlador.cancelar(); expect(controlador.reintentoPendiente, isFalse); expect(temporizadores.single.cancelado, isTrue); temporizadores.single.disparar(); expect(reintentos, 0, reason: 'un timer cancelado nunca reintenta'); }); }); group('Buffer de stream en vivo (S7-R1)', () { test('la configuracion Android usa los valores del diseno 7.1', () { const config = PluriWaveAudioHandler.configuracionCargaAndroid; final control = config.androidLoadControl; expect(control, isNotNull); expect(control!.minBufferDuration, const Duration(seconds: 15)); expect(control.maxBufferDuration, const Duration(seconds: 50)); expect( control.bufferForPlaybackDuration, const Duration(milliseconds: 2500), ); expect( control.bufferForPlaybackAfterRebufferDuration, const Duration(seconds: 5), ); expect(control.prioritizeTimeOverSizeThresholds, isTrue); }); }); }