"""
Test suite for FX Chains & Automation Pro (T061-T080)
Tests the complete FX automation system.
"""
import unittest
import sys
import os

# Add parent to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from fx_automation import (
    FXAutomationEngine,
    FXAutomationPro,
    get_fx_engine,
    FXChain,
    MacroConfig,
)


class TestT061CoreDJRack(unittest.TestCase):
    """T061: Core DJ Rack Setup tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_dj_rack_standard_created(self):
        """T061: Verificar creación de rack DJ standard."""
        rack = self.engine.create_dj_rack_config('standard')
        
        self.assertEqual(rack.name, "DJ Rack - Standard")
        self.assertEqual(len(rack.devices), 4)  # Filter, Wash, Delay, BeatMasher
        self.assertEqual(len(rack.macros), 4)
        
        # Verificar dispositivos
        device_types = [d['type'] for d in rack.devices]
        self.assertIn('AutoFilter', device_types)
        self.assertIn('HybridReverb', device_types)
        self.assertIn('Echo', device_types)
        self.assertIn('BeatRepeat', device_types)

    def test_dj_rack_extended_created(self):
        """T061: Verificar rack DJ extendido."""
        rack = self.engine.create_dj_rack_config('extended')
        
        self.assertEqual(rack.name, "DJ Rack - Extended")
        self.assertEqual(len(rack.devices), 6)  # + Flanger, Vinyl
        self.assertEqual(len(rack.macros), 6)
        
        device_types = [d['type'] for d in rack.devices]
        self.assertIn('Flanger', device_types)
        self.assertIn('VinylDistortion', device_types)

    def test_macro_configuration(self):
        """T061: Verificar configuración de macros."""
        rack = self.engine.create_dj_rack_config('standard')
        
        macro_names = [m.name for m in rack.macros]
        self.assertIn("Filter Cutoff", macro_names)
        self.assertIn("Wash Amount", macro_names)
        self.assertIn("Delay Time", macro_names)
        self.assertIn("BeatMasher", macro_names)


class TestT062BeatMasher(unittest.TestCase):
    """T062: BeatMasher Automation tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_beatmasher_quarter_pattern(self):
        """T062: Patrón quarter (1/4)."""
        bm = self.engine.create_beatmasher_automation(0, 0, 'quarter', 1.0)
        
        self.assertEqual(bm['pattern'], 'quarter')
        self.assertEqual(bm['intensity'], 1.0)
        
        # Verificar grid values en puntos
        for point in bm['points']:
            self.assertIn('time', point)
            self.assertIn('value', point)

    def test_beatmasher_eighth_pattern(self):
        """T062: Patrón eighth (1/8)."""
        bm = self.engine.create_beatmasher_automation(0, 0, 'eighth', 1.0)
        
        self.assertEqual(bm['pattern'], 'eighth')
        self.assertGreater(len(bm['points']), 0)

    def test_beatmasher_build_pattern(self):
        """T062: Patrón build para build-ups."""
        bm = self.engine.create_beatmasher_automation(0, 0, 'build', 1.0)
        
        self.assertEqual(bm['pattern'], 'build')
        # Off al final del build
        last_point = bm['points'][-1]
        self.assertEqual(last_point['value'], 0)


class TestT063TapeStop(unittest.TestCase):
    """T063: Tape Stop Automation tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_tape_stop_creation(self):
        """T063: Crear tape stop con pitch envelope."""
        ts = self.engine.create_tape_stop_automation(0, 64, 4, -12)
        
        self.assertEqual(ts['effect'], 'tape_stop')
        self.assertEqual(ts['start_time'], 64)
        self.assertEqual(ts['duration'], 4)
        self.assertEqual(ts['pitch_range'], -12)

    def test_tape_stop_pitch_curve(self):
        """T063: Verificar curva de pitch descendente."""
        ts = self.engine.create_tape_stop_automation(0, 0, 4, -12)
        
        points = ts['automation_points']
        self.assertGreater(len(points), 0)
        
        # Primer punto debe ser 0 pitch
        first_pitch = points[0]['pitch']
        self.assertAlmostEqual(first_pitch, 0, places=1)
        
        # Último punto debe ser pitch_range
        last_pitch = points[-1]['pitch']
        self.assertAlmostEqual(last_pitch, -12, places=0)


class TestT064Gater(unittest.TestCase):
    """T064: Gater/Trance Gate tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_gater_sixteenth_pattern(self):
        """T064: Patrón 1/16 gating."""
        gater = self.engine.create_gater_effect(0, 'sixteenth', '1/16', 0.8)
        
        self.assertEqual(gater['effect'], 'gater')
        self.assertEqual(gater['pattern'], 'sixteenth')
        self.assertEqual(gater['rate'], '1/16')

    def test_gater_depth_application(self):
        """T064: Profundidad de gating aplicada correctamente."""
        gater = self.engine.create_gater_effect(0, 'sixteenth', '1/16', 0.9)
        
        points = gater['automation_points']
        # Verificar que hay valores altos (abierto) y bajos (cerrado)
        values = [p['value'] for p in points]
        self.assertGreater(max(values), 0.5)
        self.assertLess(min(values), 0.5)

    def test_gater_eighth_pattern(self):
        """T064: Patrón 1/8 gating."""
        gater = self.engine.create_gater_effect(0, 'eighth', '1/8', 0.8)
        
        self.assertEqual(gater['pattern'], 'eighth')
        self.assertEqual(gater['rate'], '1/8')


class TestT065Flanger(unittest.TestCase):
    """T065: Automated Flanger Sweeps tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_flanger_sweep_creation(self):
        """T065: Crear flanger sweep."""
        flanger = self.engine.create_flanger_sweep(0, 8, 4, 'syncopated')
        
        self.assertEqual(flanger['effect'], 'flanger_sweep')
        self.assertEqual(flanger['rate'], 'syncopated')
        self.assertEqual(flanger['start_bar'], 8)
        self.assertEqual(flanger['duration_bars'], 4)

    def test_flanger_lfo_rates(self):
        """T065: Diferentes rates de LFO."""
        rates = ['slow', 'medium', 'fast', 'syncopated']
        
        for rate in rates:
            flanger = self.engine.create_flanger_sweep(0, 0, 4, rate)
            self.assertEqual(flanger['rate'], rate)
            self.assertGreater(len(flanger['automation_points']), 0)


class TestT066SendReturn(unittest.TestCase):
    """T066: Send/Return DJ Strategy tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_two_return_tracks(self):
        """T066: Configuración con 2 returns."""
        strategy = self.engine.create_dj_send_strategy(2)
        
        self.assertEqual(len(strategy['returns']), 2)
        self.assertEqual(strategy['returns'][0]['name'], 'A-Reverb')
        self.assertEqual(strategy['returns'][1]['name'], 'B-Delay')

    def test_four_return_tracks(self):
        """T066: Configuración con 4 returns."""
        strategy = self.engine.create_dj_send_strategy(4)
        
        self.assertEqual(len(strategy['returns']), 4)
        return_names = [r['name'] for r in strategy['returns']]
        self.assertIn('C-Chorus', return_names)
        self.assertIn('D-Spatial', return_names)

    def test_send_amounts_configured(self):
        """T066: Niveles de send configurados por rol."""
        strategy = self.engine.create_dj_send_strategy(4)
        
        for ret in strategy['returns']:
            self.assertIn('send_amounts', ret)
            # Bass no debe tener reverb
            if 'Reverb' in ret['name']:
                self.assertEqual(ret['send_amounts'].get('bass'), 0.0)


class TestT067MasterFilter(unittest.TestCase):
    """T067: Master Bus Filter tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_lowpass_down_sweep(self):
        """T067: Sweep lowpass descendente."""
        sweep = self.engine.create_master_filter_sweep(0, 8, 'lowpass_down')
        
        self.assertEqual(sweep['effect'], 'master_filter_sweep')
        self.assertEqual(sweep['sweep_type'], 'lowpass_down')
        self.assertEqual(sweep['track'], 'master')

    def test_lowpass_up_sweep(self):
        """T067: Sweep lowpass ascendente."""
        sweep = self.engine.create_master_filter_sweep(0, 8, 'lowpass_up')
        
        self.assertEqual(sweep['sweep_type'], 'lowpass_up')
        
        # Frecuencia debe ir de bajo a alto
        points = sweep['automation_points']
        self.assertLess(points[0]['frequency'], points[-1]['frequency'])

    def test_filter_frequency_curve(self):
        """T067: Curva logarítmica de frecuencia."""
        sweep = self.engine.create_master_filter_sweep(0, 4, 'lowpass_down')
        
        points = sweep['automation_points']
        # Verificar curva descendente
        for i in range(len(points) - 1):
            self.assertGreaterEqual(points[i]['frequency'], points[i+1]['frequency'])


class TestT068PingPongDelay(unittest.TestCase):
    """T068: Ping-Pong Delay Throws tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_pingpong_throws_creation(self):
        """T068: Crear delay throws."""
        positions = [16, 24, 32]
        throws = self.engine.create_pingpong_throws(5, positions, 0.4, True)
        
        self.assertEqual(throws['effect'], 'pingpong_throws')
        self.assertEqual(throws['track_index'], 5)
        self.assertEqual(len(throws['throws']), 3)

    def test_throw_envelope_structure(self):
        """T068: Estructura de envelope de throw."""
        positions = [16]
        throws = self.engine.create_pingpong_throws(0, positions, 0.4, True)
        
        throw = throws['throws'][0]
        self.assertIn('envelope', throw)
        self.assertGreater(len(throw['envelope']), 0)
        
        # Verificar puntos de envelope
        envelope = throw['envelope']
        self.assertEqual(envelope[-1]['value'], 0.0)  # Termina en 0

    def test_dotted_time_calculation(self):
        """T068: Cálculo de tiempo dotted."""
        throws_dotted = self.engine.create_pingpong_throws(0, [16], 0.4, True)
        throws_straight = self.engine.create_pingpong_throws(0, [16], 0.4, False)
        
        self.assertEqual(throws_dotted['delay_time'], 0.375)  # 3/8
        self.assertEqual(throws_straight['delay_time'], 0.5)  # 1/2


class TestT069Redux(unittest.TestCase):
    """T069: Redux/Bitcrusher Build tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_redux_build_creation(self):
        """T069: Crear redux build automation."""
        redux = self.engine.create_redux_build(0, 8, 16, 16, 4)
        
        self.assertEqual(redux['effect'], 'redux_build')
        self.assertEqual(redux['start_bar'], 8)
        self.assertEqual(redux['end_bar'], 16)

    def test_bit_depth_reduction(self):
        """T069: Reducción de bit depth."""
        redux = self.engine.create_redux_build(0, 0, 8, 16, 4)
        
        points = redux['automation_points']
        first_bit = points[0]['bit_depth']
        last_bit = points[-1]['bit_depth']
        
        self.assertEqual(first_bit, 16)
        self.assertEqual(last_bit, 4)

    def test_downsample_increase(self):
        """T069: Aumento de downsampling."""
        redux = self.engine.create_redux_build(0, 0, 4, 16, 4)
        
        points = redux['automation_points']
        first_ds = points[0]['downsample']
        last_ds = points[-1]['downsample']
        
        self.assertLess(first_ds, last_ds)


class TestT070Resonance(unittest.TestCase):
    """T070: Resonance Riding tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_resonance_automation_energy(self):
        """T070: Curva de energía."""
        sections = [(0, 16), (16, 32)]
        res = self.engine.create_resonance_automation(1, sections, 'energy')
        
        self.assertEqual(res['effect'], 'resonance_riding')
        self.assertEqual(res['curve_type'], 'energy')

    def test_resonance_points_per_section(self):
        """T070: Puntos por sección."""
        sections = [(0, 8)]  # 8 bars
        res = self.engine.create_resonance_automation(0, sections, 'energy')
        
        points = res['automation_points']
        # Debe haber 5 puntos clave por sección
        self.assertGreaterEqual(len(points), 5)


class TestT071Vinyl(unittest.TestCase):
    """T071: Vinyl Distortion Overlay tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_vinyl_overlay_creation(self):
        """T071: Crear vinyl overlay."""
        vinyl = self.engine.create_vinyl_overlay(0, 'medium')
        
        self.assertEqual(vinyl['effect'], 'vinyl_overlay')
        self.assertEqual(vinyl['intensity'], 'medium')

    def test_vinyl_intensity_levels(self):
        """T071: Diferentes niveles de intensidad."""
        for intensity in ['subtle', 'medium', 'heavy']:
            vinyl = self.engine.create_vinyl_overlay(0, intensity)
            self.assertEqual(vinyl['intensity'], intensity)
            self.assertIn('Crackle', vinyl['params'])

    def test_crackle_only_mode(self):
        """T071: Modo crackle only."""
        vinyl = self.engine.create_vinyl_overlay(0, 'medium', crackle_only=True)
        
        self.assertEqual(vinyl['params']['Pinch'], 0.0)


class TestT072Chorus(unittest.TestCase):
    """T072: Chorus/Widening tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_chorus_music_bus(self):
        """T072: Chorus en music bus."""
        chorus = self.engine.create_chorus_widening(2, 'music_bus', 1.2)
        
        self.assertEqual(chorus['effect'], 'chorus_widening')
        self.assertEqual(chorus['target'], 'music_bus')
        self.assertEqual(chorus['width'], 1.2)

    def test_chorus_config_by_target(self):
        """T072: Configuraciones por target."""
        targets = ['music_bus', 'vocals', 'synths', 'master']
        
        for target in targets:
            chorus = self.engine.create_chorus_widening(0, target, 1.0)
            self.assertEqual(chorus['target'], target)
            self.assertEqual(len(chorus['chain']), 2)  # Chorus + Utility


class TestT073SubBass(unittest.TestCase):
    """T073: Sub-Bass Synthesizer tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_sub_bass_creation(self):
        """T073: Crear sub-bass synth."""
        sub = self.engine.create_sub_bass_synth(1, 'Am', 'dive', [16, 48])
        
        self.assertEqual(sub['effect'], 'sub_bass_synth')
        self.assertEqual(sub['key'], 'Am')
        self.assertEqual(sub['pattern'], 'dive')

    def test_sub_bass_patterns(self):
        """T073: Diferentes patrones."""
        patterns = ['dive', 'pulse', 'sustain', 'hit']
        
        for pattern in patterns:
            sub = self.engine.create_sub_bass_synth(0, 'Am', pattern, [16])
            self.assertEqual(sub['pattern'], pattern)
            self.assertGreater(len(sub['midi_notes']), 0)

    def test_sub_bass_key_roots(self):
        """T073: Notas raíz por key."""
        keys = ['Am', 'Cm', 'Fm', 'G#m']
        
        for key in keys:
            sub = self.engine.create_sub_bass_synth(0, key, 'dive', [16])
            self.assertEqual(sub['key'], key)
            self.assertIsNotNone(sub['root_note'])


class TestT074Transient(unittest.TestCase):
    """T074: Multiband Transient Shaping tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_transient_kick_focus(self):
        """T074: Focus en kick."""
        trans = self.engine.create_transient_shaper(0, 'kick', 3.0, -2.0)
        
        self.assertEqual(trans['effect'], 'transient_shaper')
        self.assertEqual(trans['band_focus'], 'kick')

    def test_transient_band_configs(self):
        """T074: Configuraciones por banda."""
        focuses = ['kick', 'snare', 'full', 'high']
        
        for focus in focuses:
            trans = self.engine.create_transient_shaper(0, focus, 3.0, -2.0)
            self.assertEqual(trans['band_focus'], focus)
            self.assertIn('bands', trans['config'])


class TestT075Freeze(unittest.TestCase):
    """T075: Freeze FX tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_freeze_reverb_creation(self):
        """T075: Freeze con reverb."""
        freeze = self.engine.create_freeze_effect(2, 32, 2, 'reverb')
        
        self.assertEqual(freeze['effect'], 'freeze')
        self.assertEqual(freeze['source'], 'reverb')
        self.assertEqual(freeze['freeze_bar'], 32)

    def test_freeze_automation_points(self):
        """T075: Puntos de automation freeze."""
        freeze = self.engine.create_freeze_effect(0, 16, 2, 'reverb')
        
        auto_points = freeze['automation']
        self.assertEqual(len(auto_points), 3)  # Pre, activate, release
        
        # Verificar puntos
        self.assertEqual(auto_points[0]['value'], 0)
        self.assertEqual(auto_points[1]['value'], 1)
        self.assertEqual(auto_points[2]['value'], 0)


class TestT076Vocoder(unittest.TestCase):
    """T076: Vocoder Integration tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_vocoder_setup_creation(self):
        """T076: Crear setup de vocoder."""
        vocoder = self.engine.create_vocoder_setup(8, 9, 20)
        
        self.assertEqual(vocoder['effect'], 'vocoder')
        self.assertEqual(vocoder['vocoder_track'], 8)
        self.assertEqual(vocoder['carrier_track'], 9)
        self.assertEqual(vocoder['params']['Bands'], 20)

    def test_vocoder_routing(self):
        """T076: Configuración de routing."""
        vocoder = self.engine.create_vocoder_setup(5, 6, 16)
        
        self.assertEqual(vocoder['routing']['carrier'], 6)
        self.assertEqual(vocoder['routing']['modulator'], 5)


class TestT077Phaser(unittest.TestCase):
    """T077: Phaser on Hi-Hats tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_phaser_hihats_creation(self):
        """T077: Crear phaser para hi-hats."""
        phaser = self.engine.create_phaser_hihats(3, [16, 48], 8, 6)
        
        self.assertEqual(phaser['effect'], 'phaser_hihats')
        self.assertEqual(phaser['params']['Stages'], 6)

    def test_phaser_sweeps_count(self):
        """T077: Cantidad de sweeps."""
        positions = [16, 32, 48]
        phaser = self.engine.create_phaser_hihats(0, positions, 8, 6)
        
        self.assertEqual(len(phaser['sweeps']), 3)

    def test_phaser_stages(self):
        """T077: Diferentes stages."""
        for stages in [2, 4, 6, 8, 12]:
            phaser = self.engine.create_phaser_hihats(0, [16], 4, stages)
            self.assertEqual(phaser['params']['Stages'], stages)


class TestT078Saturation(unittest.TestCase):
    """T078: Saturation Drive tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_saturation_master_bus(self):
        """T078: Saturación en master."""
        sat = self.engine.create_saturation_drive(-1, 2.0, 'master')
        
        self.assertEqual(sat['effect'], 'saturation_drive')
        self.assertEqual(sat['target'], 'master')
        self.assertEqual(sat['drive_db'], 2.0)

    def test_saturation_targets(self):
        """T078: Diferentes targets."""
        targets = ['master', 'drums', 'bass', 'music']
        
        for target in targets:
            sat = self.engine.create_saturation_drive(0, 2.0, target)
            self.assertEqual(sat['target'], target)
            self.assertEqual(sat['device'], 'Saturator')

    def test_saturation_drive_values(self):
        """T078: Valores de drive."""
        for drive in [1.0, 2.0, 4.0, 6.0]:
            sat = self.engine.create_saturation_drive(0, drive, 'master')
            self.assertEqual(sat['drive_db'], drive)


class TestT079AutoPan(unittest.TestCase):
    """T079: Auto-Pan Rhythms tests."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_autopan_triplets(self):
        """T079: Auto-pan con triplets."""
        pan = self.engine.create_autopan_rhythm(4, 'triplets')
        
        self.assertEqual(pan['effect'], 'autopan_rhythm')
        self.assertEqual(pan['rhythm'], 'triplets')
        self.assertEqual(pan['device'], 'AutoPan')

    def test_autopan_rates(self):
        """T079: Diferentes ritmos."""
        rhythms = ['straight', 'triplets', 'dotted']
        
        for rhythm in rhythms:
            pan = self.engine.create_autopan_rhythm(0, rhythm)
            self.assertEqual(pan['rhythm'], rhythm)

    def test_autopan_amount_automation(self):
        """T079: Automation de amount por sección."""
        pan = self.engine.create_autopan_rhythm(0, 'triplets')
        
        auto = pan['automation']
        self.assertGreater(len(auto), 0)
        
        sections = [a['section'] for a in auto]
        self.assertIn('intro', sections)
        self.assertIn('drop', sections)


class TestT080Integration(unittest.TestCase):
    """T080: Integration Test - FX Medley."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_medley_creation(self):
        """T080: Crear FX medley completo."""
        medley = self.engine.create_fx_medley_test(128, 'Am')
        
        self.assertEqual(medley['name'], 'FX Medley Test')
        self.assertEqual(medley['bpm'], 128)
        self.assertEqual(medley['key'], 'Am')

    def test_medley_sections(self):
        """T080: Secciones del medley."""
        medley = self.engine.create_fx_medley_test(128, 'Am')
        
        sections = medley['sections']
        self.assertEqual(len(sections), 6)
        
        section_names = [s['name'] for s in sections]
        self.assertIn('intro', section_names)
        self.assertIn('drop_a', section_names)
        self.assertIn('break', section_names)

    def test_medley_tracks(self):
        """T080: Tracks con FX chains."""
        medley = self.engine.create_fx_medley_test(128, 'Am')
        
        tracks = medley['tracks']
        self.assertGreater(len(tracks), 0)
        
        roles = [t['role'] for t in tracks]
        self.assertIn('drums', roles)
        self.assertIn('bass', roles)
        self.assertIn('music', roles)

    def test_medley_transitions(self):
        """T080: Transiciones configuradas."""
        medley = self.engine.create_fx_medley_test(128, 'Am')
        
        transitions = medley['transitions']
        self.assertGreater(len(transitions), 0)
        
        for trans in transitions:
            self.assertIn('from', trans)
            self.assertIn('to', trans)
            self.assertIn('fx', trans)

    def test_global_engine(self):
        """T080: Instancia global del engine."""
        engine1 = get_fx_engine(42)
        engine2 = get_fx_engine(42)
        
        self.assertIs(engine1, engine2)  # Misma instancia

    def test_all_fx_configs(self):
        """T080: Obtener todas las configuraciones."""
        configs = self.engine.get_all_fx_configs()
        
        self.assertIn('T061_dj_rack_standard', configs)
        self.assertIn('T061_dj_rack_extended', configs)


class TestFXEngineEdgeCases(unittest.TestCase):
    """Casos edge y validación."""

    def setUp(self):
        self.engine = FXAutomationEngine(seed=42)

    def test_invalid_pattern_defaults(self):
        """Pattern inválido usa default."""
        gater = self.engine.create_gater_effect(0, 'invalid', '1/16', 0.8)
        # Debe usar patrón aleatorio
        self.assertEqual(gater['effect'], 'gater')

    def test_empty_positions_list(self):
        """Lista vacía de posiciones."""
        throws = self.engine.create_pingpong_throws(0, [], 0.4, True)
        self.assertEqual(len(throws['throws']), 0)

    def test_negative_drive_handling(self):
        """Manejo de drive negativo."""
        sat = self.engine.create_saturation_drive(0, -5.0, 'master')
        # Debe clamp a valor mínimo
        self.assertGreaterEqual(sat['params']['Drive'], 0.5)


if __name__ == '__main__':
    # Run tests
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()

    # Add all test classes
    test_classes = [
        TestT061CoreDJRack,
        TestT062BeatMasher,
        TestT063TapeStop,
        TestT064Gater,
        TestT065Flanger,
        TestT066SendReturn,
        TestT067MasterFilter,
        TestT068PingPongDelay,
        TestT069Redux,
        TestT070Resonance,
        TestT071Vinyl,
        TestT072Chorus,
        TestT073SubBass,
        TestT074Transient,
        TestT075Freeze,
        TestT076Vocoder,
        TestT077Phaser,
        TestT078Saturation,
        TestT079AutoPan,
        TestT080Integration,
        TestFXEngineEdgeCases,
    ]

    for test_class in test_classes:
        tests = loader.loadTestsFromTestCase(test_class)
        suite.addTests(tests)

    # Run with verbosity
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)

    # Exit with appropriate code
    sys.exit(0 if result.wasSuccessful() else 1)
