import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_ecualizador.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/modelos/dispositivo_audio.dart'; import 'package:pluriwave/modelos/preset_ecualizador.dart'; import '../helpers/fakes.dart'; void main() { group('EstadoEcualizador (S4-R1)', () { test( 'cambiarPreset notifies EstadoEcualizador listeners (S4-R1-A)', () async { final audio = FakeServicioAudio(); final eq = EstadoEcualizador( audio: audio, servicio: FakeServicioEcualizador(), ); var avisos = 0; eq.addListener(() => avisos++); await eq.cambiarPreset(PresetEcualizador.jazz); expect(avisos, greaterThanOrEqualTo(1)); expect(eq.presetActual, PresetEcualizador.jazz); expect(audio.presetsAplicados, contains(PresetEcualizador.jazz)); eq.dispose(); }, ); test('EQ preset change does NOT rebuild EstadoRadio listeners ' '(S4-R1-A, S4-R5)', () async { final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), iniciarAutomaticamente: false, ); var avisosRadio = 0; var avisosEq = 0; estado.addListener(() => avisosRadio++); estado.ecualizador.addListener(() => avisosEq++); await estado.ecualizador.cambiarPreset(PresetEcualizador.jazz); expect(avisosEq, greaterThanOrEqualTo(1)); expect(avisosRadio, 0); estado.dispose(); }); }); // --------------------------------------------------------------------------- // Phase 5: 4-level resolution hierarchy // --------------------------------------------------------------------------- group('EstadoEcualizador — 4-level resolution (Phase 5)', () { const btDevice = DispositivoAudio( id: 'bt_a2dp:AA:BB:CC:DD:EE:FF', tipo: TipoDispositivo.bluetoothA2dp, nombre: 'BT Speaker', ); const stationUuid = 'station-uuid-123'; EstadoEcualizador buildEq({ FakeServicioEcualizador? servicio, FakeServicioDispositivoAudio? dispositivoAudio, bool multiDeviceEnabled = true, String? emisoraActualUuid, }) { return EstadoEcualizador( audio: FakeServicioAudio(), servicio: servicio ?? FakeServicioEcualizador(eqMultiDeviceEnabled: multiDeviceEnabled), dispositivoAudio: dispositivoAudio ?? FakeServicioDispositivoAudio(), emisoraActualUuid: () => emisoraActualUuid, ); } // 5.1a — matrix entry wins over all others test( '5.1a matrix entry (stationUuid:deviceId) wins top priority', () async { final matrizPreset = PresetEcualizador.bassBoost; final stationPreset = PresetEcualizador.rock; final devicePreset = PresetEcualizador.jazz; final globalPreset = PresetEcualizador.pop; final eq = buildEq( servicio: FakeServicioEcualizador( principal: globalPreset, porEmisora: {stationUuid: stationPreset}, eqMultiDeviceEnabled: true, presetsDispositivo: {btDevice.id: devicePreset}, presetsMatriz: {'$stationUuid:${btDevice.id}': matrizPreset}, ), emisoraActualUuid: stationUuid, ); await eq.cargarPersistido(); expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(matrizPreset), ); eq.dispose(); }, ); // 5.1b — falls back to station when no matrix entry test('5.1b falls back to station preset when no matrix entry', () async { final stationPreset = PresetEcualizador.rock; final devicePreset = PresetEcualizador.jazz; final globalPreset = PresetEcualizador.pop; final eq = buildEq( servicio: FakeServicioEcualizador( principal: globalPreset, porEmisora: {stationUuid: stationPreset}, eqMultiDeviceEnabled: true, presetsDispositivo: {btDevice.id: devicePreset}, presetsMatriz: {}, ), emisoraActualUuid: stationUuid, ); await eq.cargarPersistido(); expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(stationPreset), ); eq.dispose(); }); // 5.1c — falls back to device when no station preset test( '5.1c falls back to device preset when no station or matrix entry', () async { final devicePreset = PresetEcualizador.jazz; final globalPreset = PresetEcualizador.pop; final eq = buildEq( servicio: FakeServicioEcualizador( principal: globalPreset, porEmisora: {}, eqMultiDeviceEnabled: true, presetsDispositivo: {btDevice.id: devicePreset}, presetsMatriz: {}, ), emisoraActualUuid: stationUuid, ); await eq.cargarPersistido(); expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(devicePreset), ); eq.dispose(); }, ); // 5.1d — falls back to global when nothing else matches test( '5.1d falls back to global preset when no matrix, station, or device', () async { final globalPreset = PresetEcualizador.pop; final eq = buildEq( servicio: FakeServicioEcualizador( principal: globalPreset, porEmisora: {}, eqMultiDeviceEnabled: true, presetsDispositivo: {}, presetsMatriz: {}, ), emisoraActualUuid: stationUuid, ); await eq.cargarPersistido(); expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(globalPreset), ); eq.dispose(); }, ); // 5.1e — toggle off collapses to 2-level test( '5.1e toggle off: presetEfectivo uses only station → global', () async { final stationPreset = PresetEcualizador.rock; final devicePreset = PresetEcualizador.jazz; final matrizPreset = PresetEcualizador.bassBoost; final eq = buildEq( servicio: FakeServicioEcualizador( principal: PresetEcualizador.flat, porEmisora: {stationUuid: stationPreset}, eqMultiDeviceEnabled: false, presetsDispositivo: {btDevice.id: devicePreset}, presetsMatriz: {'$stationUuid:${btDevice.id}': matrizPreset}, ), multiDeviceEnabled: false, emisoraActualUuid: stationUuid, ); await eq.cargarPersistido(); // When toggle is off, device and matrix entries must be ignored expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(stationPreset), ); eq.dispose(); }, ); // 5.1f — toggle off with no station falls back to global test('5.1f toggle off with no station: falls back to global', () async { final devicePreset = PresetEcualizador.jazz; final eq = buildEq( servicio: FakeServicioEcualizador( principal: PresetEcualizador.flat, porEmisora: {}, eqMultiDeviceEnabled: false, presetsDispositivo: {btDevice.id: devicePreset}, presetsMatriz: {}, ), multiDeviceEnabled: false, emisoraActualUuid: stationUuid, ); await eq.cargarPersistido(); expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(PresetEcualizador.flat), ); eq.dispose(); }); }); // --------------------------------------------------------------------------- // Phase 5.3: Device stream subscription triggers re-resolution // --------------------------------------------------------------------------- group('EstadoEcualizador — device stream subscription (Phase 5.3–5.4)', () { const stationUuid = 'station-uuid-stream'; const btDevice = DispositivoAudio( id: 'bt_a2dp:AA:BB:CC:DD:EE:FF', tipo: TipoDispositivo.bluetoothA2dp, nombre: 'BT Speaker', ); test('5.3a device change emits notifyListeners', () async { final fakeAudio = FakeServicioAudio(); final fakeDispositivo = FakeServicioDispositivoAudio(); final eq = EstadoEcualizador( audio: fakeAudio, servicio: FakeServicioEcualizador( eqMultiDeviceEnabled: true, presetsDispositivo: {btDevice.id: PresetEcualizador.jazz}, ), dispositivoAudio: fakeDispositivo, emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); var avisos = 0; eq.addListener(() => avisos++); fakeDispositivo.emitirDispositivo(btDevice); // Wait for async processing await Future.delayed(const Duration(milliseconds: 50)); expect(avisos, greaterThanOrEqualTo(1)); eq.dispose(); }); test( '5.3b device change re-applies resolved preset to audio engine', () async { final fakeAudio = FakeServicioAudio(); final fakeDispositivo = FakeServicioDispositivoAudio(); final btPreset = PresetEcualizador.jazz; final eq = EstadoEcualizador( audio: fakeAudio, servicio: FakeServicioEcualizador( eqMultiDeviceEnabled: true, presetsDispositivo: {btDevice.id: btPreset}, ), dispositivoAudio: fakeDispositivo, emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); final initialCount = fakeAudio.presetsAplicados.length; fakeDispositivo.emitirDispositivo(btDevice); await Future.delayed(const Duration(milliseconds: 50)); expect(fakeAudio.presetsAplicados.length, greaterThan(initialCount)); expect(fakeAudio.presetsAplicados.last, equals(btPreset)); eq.dispose(); }, ); test( '5.3c toggle off: device stream events do NOT trigger any action', () async { final fakeAudio = FakeServicioAudio(); final fakeDispositivo = FakeServicioDispositivoAudio(); final eq = EstadoEcualizador( audio: fakeAudio, servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false), dispositivoAudio: fakeDispositivo, emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); var avisos = 0; eq.addListener(() => avisos++); final initialCount = fakeAudio.presetsAplicados.length; fakeDispositivo.emitirDispositivo(btDevice); await Future.delayed(const Duration(milliseconds: 50)); expect(avisos, equals(0)); expect(fakeAudio.presetsAplicados.length, equals(initialCount)); eq.dispose(); }, ); }); // --------------------------------------------------------------------------- // Phase 5.5–5.6: First-seen device copies current preset // --------------------------------------------------------------------------- group( 'EstadoEcualizador — first-seen device initialization (Phase 5.5–5.6)', () { const stationUuid = 'station-uuid-first-seen'; const newDevice = DispositivoAudio( id: 'bt_a2dp:NEW:DE:VI:CE:00:00', tipo: TipoDispositivo.bluetoothA2dp, nombre: 'New BT Device', ); test('5.5a new device gets copy of current resolved preset', () async { final fakeServicio = FakeServicioEcualizador( principal: PresetEcualizador.rock, eqMultiDeviceEnabled: true, presetsDispositivo: {}, ); final fakeDispositivo = FakeServicioDispositivoAudio(); final eq = EstadoEcualizador( audio: FakeServicioAudio(), servicio: fakeServicio, dispositivoAudio: fakeDispositivo, emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); fakeDispositivo.emitirDispositivo(newDevice); await Future.delayed(const Duration(milliseconds: 50)); // The device should now have a preset saved equal to the resolved preset // at the time of first connection (rock — global fallback) expect( eq.presetsDispositivo[newDevice.id], equals(PresetEcualizador.rock), ); eq.dispose(); }); test( '5.5b subsequent reconnection does NOT overwrite user-saved preset', () async { final userSavedPreset = PresetEcualizador.jazz; final fakeServicio = FakeServicioEcualizador( principal: PresetEcualizador.rock, eqMultiDeviceEnabled: true, presetsDispositivo: {newDevice.id: userSavedPreset}, ); final fakeDispositivo = FakeServicioDispositivoAudio(); final eq = EstadoEcualizador( audio: FakeServicioAudio(), servicio: fakeServicio, dispositivoAudio: fakeDispositivo, emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); fakeDispositivo.emitirDispositivo(newDevice); await Future.delayed(const Duration(milliseconds: 50)); // Preset must remain the user-saved one, not overwritten with rock expect(eq.presetsDispositivo[newDevice.id], equals(userSavedPreset)); eq.dispose(); }, ); }, ); // --------------------------------------------------------------------------- // Phase 5.7–5.8: Toggle-off path uses only 2-level resolution // --------------------------------------------------------------------------- group('EstadoEcualizador — toggle-off path (Phase 5.7–5.8)', () { const stationUuid = 'station-toggle-off'; const btDevice = DispositivoAudio( id: 'bt_a2dp:AA:BB:CC:DD:EE:FF', tipo: TipoDispositivo.bluetoothA2dp, nombre: 'BT Speaker', ); test( '5.7a cargarPersistido with toggle off applies 2-level resolution', () async { final fakeAudio = FakeServicioAudio(); final stationPreset = PresetEcualizador.rock; final devicePreset = PresetEcualizador.jazz; final eq = EstadoEcualizador( audio: fakeAudio, servicio: FakeServicioEcualizador( principal: PresetEcualizador.flat, porEmisora: {stationUuid: stationPreset}, eqMultiDeviceEnabled: false, presetsDispositivo: {btDevice.id: devicePreset}, ), dispositivoAudio: FakeServicioDispositivoAudio(), emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); // With toggle off, no device subscription, direct 2-level on presetEfectivo expect( eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id), equals(stationPreset), ); eq.dispose(); }, ); test('5.8a eqMultiDeviceEnabled getter reflects loaded value', () async { final eq = EstadoEcualizador( audio: FakeServicioAudio(), servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true), dispositivoAudio: FakeServicioDispositivoAudio(), ); await eq.cargarPersistido(); expect(eq.eqMultiDeviceEnabled, isTrue); eq.dispose(); }); test('5.8b toggle defaults to false and eq behaves as before', () async { final eq = EstadoEcualizador( audio: FakeServicioAudio(), servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false), dispositivoAudio: FakeServicioDispositivoAudio(), ); await eq.cargarPersistido(); expect(eq.eqMultiDeviceEnabled, isFalse); eq.dispose(); }); test( '5.8c cambiarMultiDeviceEnabled persists toggle and notifies', () async { final fakeServicio = FakeServicioEcualizador( eqMultiDeviceEnabled: false, ); final eq = EstadoEcualizador( audio: FakeServicioAudio(), servicio: fakeServicio, dispositivoAudio: FakeServicioDispositivoAudio(), ); await eq.cargarPersistido(); var avisos = 0; eq.addListener(() => avisos++); await eq.cambiarMultiDeviceEnabled(true); expect(eq.eqMultiDeviceEnabled, isTrue); expect(fakeServicio.config.eqMultiDeviceEnabled, isTrue); expect(avisos, greaterThanOrEqualTo(1)); eq.dispose(); }, ); }); // --------------------------------------------------------------------------- // Phase 5.9: REFACTOR — aplicarPresetActivo path applies device-resolved preset // --------------------------------------------------------------------------- group( 'EstadoEcualizador — aplicarPresetActivo uses device resolution (5.9)', () { const stationUuid = 'station-refactor'; const btDevice = DispositivoAudio( id: 'bt_a2dp:AA:BB:CC:DD:EE:FF', tipo: TipoDispositivo.bluetoothA2dp, nombre: 'BT Speaker', ); test( '5.9a aplicarPresetEmisoraActual uses device-resolved preset when toggle on', () async { final fakeAudio = FakeServicioAudio(); final fakeDispositivo = FakeServicioDispositivoAudio()..emitirDispositivo(btDevice); final matrizPreset = PresetEcualizador.bassBoost; final eq = EstadoEcualizador( audio: fakeAudio, servicio: FakeServicioEcualizador( eqMultiDeviceEnabled: true, presetsMatriz: {'$stationUuid:${btDevice.id}': matrizPreset}, ), dispositivoAudio: fakeDispositivo, emisoraActualUuid: () => stationUuid, ); await eq.cargarPersistido(); await Future.delayed(const Duration(milliseconds: 50)); // Clear application history from init; emit device to trigger re-apply fakeAudio.presetsAplicados.clear(); fakeDispositivo.emitirDispositivo(btDevice); await Future.delayed(const Duration(milliseconds: 50)); expect(fakeAudio.presetsAplicados.last, equals(matrizPreset)); eq.dispose(); }, ); }, ); }