import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/controlador_reconexion.dart'; import 'package:pluriwave/servicios/servicio_audio.dart'; /// S6-R3 — Source-revision guard: rapid source switches must not let a stale /// error from an earlier source bleed through to the final active source. /// /// [PluriWaveAudioHandler] uses a monotonically-increasing [_revisionFuente] /// counter; each [playMediaItem] call increments it and checks `revision == /// _revisionFuente` before every async step in [_cambiarFuente]. We cannot /// instantiate the handler in unit tests (MethodChannels), so we exercise the /// guard invariants through: /// (a) the static buffer-configuration constant (already fully testable), and /// (b) [ControladorReconexion.restablecer], which [playMediaItem] calls on /// every fresh user switch to discard any in-flight backoff for the /// previous source — the core "no stale error bleeds through" contract. void main() { group('Source-switch guard (S6-R3)', () { test( 'rapid switch resets the reconnect controller so old backoff is discarded', () { final temporizadores = <_FakeTimer>[]; final controlador = ControladorReconexion( crearTemporizador: (duracion, cb) { final t = _FakeTimer(duracion, cb); temporizadores.add(t); return t; }, ); // Simulate source A failing and scheduling a retry. controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); expect(controlador.reintentoPendiente, isTrue); // User rapidly switches to source B — playMediaItem calls restablecer(). controlador.restablecer(); expect( controlador.reintentoPendiente, isFalse, reason: 'stale source-A retry must not fire after switching to B', ); expect( controlador.intentos, 0, reason: 'backoff counter resets so source B starts with a clean slate', ); expect( temporizadores.single.cancelado, isTrue, reason: 'the pending timer for source A was cancelled', ); }, ); test( 'three rapid switches all reset backoff independently (A→B→C keeps C clean)', () { final controlador = ControladorReconexion(); // A fails, schedule retry. controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); // Switch to B — resets. controlador.restablecer(); // B fails immediately too. controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); // Switch to C — resets again. controlador.restablecer(); expect(controlador.reintentoPendiente, isFalse); expect(controlador.intentos, 0); }, ); test( 'buffer config constant carries the live-stream guard values (S7-R1)', () { // Ensures the static config used when recreating the player for a // source switch carries the correct live-stream buffer parameters. 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)); }, ); }); } class _FakeTimer implements Timer { _FakeTimer(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; }