import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/modelos/pista_local.dart'; import 'package:pluriwave/servicios/cola_local.dart'; /// [PluriWaveAudioHandler] cannot be instantiated in unit tests (its /// constructor builds a real `just_audio.AudioPlayer` needing platform /// `MethodChannel`s — confirmed by `servicio_audio_eq_reapply_test.dart` and /// `servicio_audio_source_switch_test.dart`). `cola_local.dart` extracts the /// queue-advance DECISION logic into pure, side-effect-free functions — /// mirroring how `ControladorReconexion` was extracted from /// `servicio_audio.dart` — so it is fully unit-testable here; the actual /// field mutation on the handler is static-review-only (Phase 3 tasks). void main() { const n0 = NodoLocal(documentId: 'd0', nombre: 'a.mp3', esDirectorio: false); const n1 = NodoLocal(documentId: 'd1', nombre: 'b.mp3', esDirectorio: false); const n2 = NodoLocal(documentId: 'd2', nombre: 'c.mp3', esDirectorio: false); group('ColaLocal', () { test('cola vacía: hayActual es false, conSiguiente es null', () { const cola = ColaLocal(pistas: []); expect(cola.hayActual, isFalse); expect(cola.conSiguiente(), isNull); }); test('cola vacía: conAnterior se clampa en 0 sin lanzar', () { const cola = ColaLocal(pistas: []); expect(cola.conAnterior().indice, 0); }); test('un solo item: conSiguiente es null (ya es el único/último)', () { const cola = ColaLocal(pistas: [n0]); expect(cola.hayActual, isTrue); expect(cola.actual, same(n0)); expect(cola.conSiguiente(), isNull); }); test('un solo item: conAnterior se clampa en 0 (reinicia la pista)', () { const cola = ColaLocal(pistas: [n0]); expect(cola.conAnterior().indice, 0); }); test('medio de la lista: conSiguiente avanza el índice y expone la ' 'pista siguiente', () { const cola = ColaLocal(pistas: [n0, n1, n2], indice: 0); final siguiente = cola.conSiguiente(); expect(siguiente, isNotNull); expect(siguiente!.indice, 1); expect(siguiente.actual, same(n1)); }); test('medio de la lista: conAnterior retrocede el índice', () { const cola = ColaLocal(pistas: [n0, n1, n2], indice: 2); final anterior = cola.conAnterior(); expect(anterior.indice, 1); expect(anterior.actual, same(n1)); }); test('límite: último índice, conSiguiente es null (fin de cola, sin ' 'loop — Design ADR-4)', () { const cola = ColaLocal(pistas: [n0, n1], indice: 1); expect(cola.conSiguiente(), isNull); }); test('límite: primer índice, conAnterior se clampa en 0 (no da ' 'negativo)', () { const cola = ColaLocal(pistas: [n0, n1], indice: 0); expect(cola.conAnterior().indice, 0); }); test('conSiguiente/conAnterior devuelven una instancia NUEVA, nunca ' 'mutan la original', () { const original = ColaLocal(pistas: [n0, n1, n2], indice: 0); final siguiente = original.conSiguiente(); expect(original.indice, 0, reason: 'la instancia original no cambia'); expect(siguiente, isNot(same(original))); }); }); group('decidirAvanceCola', () { test('colaLocal == null => ninguna (proxy de aislamiento de radio: ' 'radio nunca setea _colaLocal)', () { expect( decidirAvanceCola( colaLocal: null, avanzandoCola: false, trackCompletado: true, ), DecisionAvanceCola.ninguna, ); }); test('trackCompletado == false => ninguna, sin importar el resto ' '(estado buffering/ready no dispara avance)', () { const cola = ColaLocal(pistas: [n0, n1]); expect( decidirAvanceCola( colaLocal: cola, avanzandoCola: false, trackCompletado: false, ), DecisionAvanceCola.ninguna, ); }); test('avanzandoCola == true => ninguna (latch de re-entrada: dos ' 'emisiones "completado" rápidas producen un solo avance)', () { const cola = ColaLocal(pistas: [n0, n1]); final primera = decidirAvanceCola( colaLocal: cola, avanzandoCola: false, trackCompletado: true, ); expect(primera, DecisionAvanceCola.avanzar); // Simula una segunda emisión "completado" mientras el primer avance // sigue resolviendo su URI (el llamador ya seteó el latch en true). final segunda = decidirAvanceCola( colaLocal: cola, avanzandoCola: true, trackCompletado: true, ); expect(segunda, DecisionAvanceCola.ninguna); }); test('conSiguiente() == null => desactivar (fin de cola)', () { const cola = ColaLocal(pistas: [n0, n1], indice: 1); expect( decidirAvanceCola( colaLocal: cola, avanzandoCola: false, trackCompletado: true, ), DecisionAvanceCola.desactivar, ); }); test('hay una pista siguiente => avanzar', () { const cola = ColaLocal(pistas: [n0, n1], indice: 0); expect( decidirAvanceCola( colaLocal: cola, avanzandoCola: false, trackCompletado: true, ), DecisionAvanceCola.avanzar, ); }); }); group('avanceEsValido', () { test('la misma instancia => true', () { const cola = ColaLocal(pistas: [n0]); expect(avanceEsValido(cola, cola), isTrue); }); test('instancias distintas aunque estructuralmente iguales => false ' '(guarda contra un swap accidental de == por identical)', () { // Non-const construction is deliberate: two `const` instances with // identical field values are canonicalized to the SAME object by // Dart, which would make this assertion pass for the wrong reason. // A real mid-await race produces two genuinely distinct instances // (e.g. one from an auto-advance, one from a user's `playMediaItem` // during the await), so non-const `ColaLocal(...)` is the accurate // reproduction. final a = ColaLocal(pistas: [n0, n1], indice: 1); final b = ColaLocal(pistas: [n0, n1], indice: 1); expect(avanceEsValido(a, b), isFalse); }); test('null vs no-null => false', () { const cola = ColaLocal(pistas: [n0]); expect(avanceEsValido(null, cola), isFalse); expect(avanceEsValido(cola, null), isFalse); }); test('ambos null => true (ningún lado tiene cola activa)', () { expect(avanceEsValido(null, null), isTrue); }); }); }