"""Tests for audio filters and the AudioFilterPipeline."""

import numpy as np
import pytest

from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter
from ledgrab.core.audio.filters.pipeline import AudioFilterPipeline
from ledgrab.core.audio.filters.registry import AudioFilterRegistry

# Import the package to trigger auto-registration of all built-in filters
import ledgrab.core.audio.filters  # noqa: F401

from ledgrab.core.filters.filter_instance import FilterInstance

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_analysis(
    rms: float = 0.5,
    peak: float = 0.7,
    spectrum: np.ndarray | None = None,
    beat: bool = False,
    beat_intensity: float = 0.0,
    left_rms: float = 0.3,
    right_rms: float = 0.6,
    left_spectrum: np.ndarray | None = None,
    right_spectrum: np.ndarray | None = None,
) -> AudioAnalysis:
    """Build an AudioAnalysis with sensible defaults for testing."""
    if spectrum is None:
        spectrum = np.linspace(0.0, 1.0, NUM_BANDS, dtype=np.float32)
    if left_spectrum is None:
        left_spectrum = np.full(NUM_BANDS, 0.3, dtype=np.float32)
    if right_spectrum is None:
        right_spectrum = np.full(NUM_BANDS, 0.6, dtype=np.float32)
    return AudioAnalysis(
        timestamp=1.0,
        rms=rms,
        peak=peak,
        spectrum=spectrum,
        beat=beat,
        beat_intensity=beat_intensity,
        left_rms=left_rms,
        left_spectrum=left_spectrum,
        right_rms=right_rms,
        right_spectrum=right_spectrum,
    )


def _zero_analysis() -> AudioAnalysis:
    """AudioAnalysis with all zeros."""
    return AudioAnalysis(timestamp=1.0)


# ---------------------------------------------------------------------------
# Registry tests
# ---------------------------------------------------------------------------


class TestAudioFilterRegistry:
    def test_all_built_in_filters_registered(self):
        expected = {
            "channel_extract",
            "band_extract",
            "gain",
            "inverter",
            "peak_hold",
            "noise_gate",
            "envelope_follower",
            "spectral_smoothing",
            "compressor",
            "beat_gate",
            "delay",
            "audio_filter_template",
        }
        registered = set(AudioFilterRegistry.get_all().keys())
        assert expected.issubset(registered), f"Missing: {expected - registered}"

    def test_create_instance(self):
        f = AudioFilterRegistry.create_instance("gain", {"factor": 2.0})
        assert isinstance(f, AudioFilter)
        assert f.options["factor"] == 2.0

    def test_create_unknown_raises(self):
        with pytest.raises(ValueError, match="Unknown audio filter type"):
            AudioFilterRegistry.create_instance("nonexistent", {})

    def test_is_registered(self):
        assert AudioFilterRegistry.is_registered("gain")
        assert not AudioFilterRegistry.is_registered("nonexistent")


# ---------------------------------------------------------------------------
# Channel Extract filter
# ---------------------------------------------------------------------------


class TestChannelExtractFilter:
    def test_mono_averages_channels(self):
        a = _make_analysis(left_rms=0.2, right_rms=0.8)
        f = AudioFilterRegistry.create_instance("channel_extract", {"channel": "mono"})
        result = f.process(a)
        assert pytest.approx(result.rms, abs=1e-5) == 0.5

    def test_left_channel(self):
        a = _make_analysis(left_rms=0.2, right_rms=0.8)
        f = AudioFilterRegistry.create_instance("channel_extract", {"channel": "left"})
        result = f.process(a)
        assert result.rms == 0.2

    def test_right_channel(self):
        a = _make_analysis(left_rms=0.2, right_rms=0.8)
        f = AudioFilterRegistry.create_instance("channel_extract", {"channel": "right"})
        result = f.process(a)
        assert result.rms == 0.8

    def test_does_not_mutate_input(self):
        a = _make_analysis()
        orig_rms = a.rms
        f = AudioFilterRegistry.create_instance("channel_extract", {"channel": "left"})
        f.process(a)
        assert a.rms == orig_rms


# ---------------------------------------------------------------------------
# Band Extract filter
# ---------------------------------------------------------------------------


class TestBandExtractFilter:
    def test_bass_preset_zeroes_high_bins(self):
        a = _make_analysis()
        f = AudioFilterRegistry.create_instance("band_extract", {"band": "bass"})
        result = f.process(a)
        # Top spectrum bins should be zeroed
        assert result.spectrum[-1] == 0.0

    def test_treble_preset_zeroes_low_bins(self):
        a = _make_analysis()
        f = AudioFilterRegistry.create_instance("band_extract", {"band": "treble"})
        result = f.process(a)
        # Bottom spectrum bins should be zeroed
        assert result.spectrum[0] == 0.0

    def test_zero_input_stays_zero(self):
        a = _zero_analysis()
        f = AudioFilterRegistry.create_instance("band_extract", {"band": "bass"})
        result = f.process(a)
        assert result.rms == 0.0
        assert np.all(result.spectrum == 0.0)


# ---------------------------------------------------------------------------
# Gain filter
# ---------------------------------------------------------------------------


class TestGainFilter:
    def test_unity_gain_passthrough(self):
        a = _make_analysis(rms=0.5)
        f = AudioFilterRegistry.create_instance("gain", {"factor": 1.0})
        result = f.process(a)
        # Unity gain returns the same object
        assert result is a

    def test_double_gain(self):
        a = _make_analysis(rms=0.3, peak=0.4)
        f = AudioFilterRegistry.create_instance("gain", {"factor": 2.0})
        result = f.process(a)
        assert pytest.approx(result.rms) == 0.6
        assert pytest.approx(result.peak) == 0.8

    def test_gain_clamps_to_one(self):
        a = _make_analysis(rms=0.8)
        f = AudioFilterRegistry.create_instance("gain", {"factor": 5.0})
        result = f.process(a)
        assert result.rms <= 1.0

    def test_gain_clamps_spectrum(self):
        a = _make_analysis()
        f = AudioFilterRegistry.create_instance("gain", {"factor": 10.0})
        result = f.process(a)
        assert np.all(result.spectrum <= 1.0)
        assert np.all(result.spectrum >= 0.0)


# ---------------------------------------------------------------------------
# Inverter filter
# ---------------------------------------------------------------------------


class TestInverterFilter:
    def test_invert_rms(self):
        a = _make_analysis(rms=0.3, peak=0.7)
        f = AudioFilterRegistry.create_instance("inverter", {})
        result = f.process(a)
        assert pytest.approx(result.rms, abs=1e-6) == 0.7
        assert pytest.approx(result.peak, abs=1e-6) == 0.3

    def test_invert_spectrum(self):
        a = _make_analysis()
        f = AudioFilterRegistry.create_instance("inverter", {"invert_spectrum": True})
        result = f.process(a)
        np.testing.assert_allclose(result.spectrum, 1.0 - a.spectrum, atol=1e-6)

    def test_no_spectrum_inversion(self):
        a = _make_analysis()
        f = AudioFilterRegistry.create_instance("inverter", {"invert_spectrum": False})
        result = f.process(a)
        np.testing.assert_array_equal(result.spectrum, a.spectrum)


# ---------------------------------------------------------------------------
# Peak Hold filter (stateful)
# ---------------------------------------------------------------------------


class TestPeakHoldFilter:
    def test_is_stateful(self):
        f = AudioFilterRegistry.create_instance("peak_hold", {})
        assert f.is_stateful is True

    def test_holds_peak_value(self):
        f = AudioFilterRegistry.create_instance("peak_hold", {"decay_rate": 0.1})
        # First: high value
        a1 = _make_analysis(rms=0.9, peak=0.9)
        r1 = f.process(a1)
        assert r1.rms >= 0.89
        # Second: low value — should still hold near the peak (tiny dt, minimal decay)
        a2 = _make_analysis(rms=0.1, peak=0.1)
        r2 = f.process(a2)
        # Held value should be very close to 0.9 (only microseconds of decay)
        assert r2.rms >= 0.85

    def test_reset_clears_state(self):
        f = AudioFilterRegistry.create_instance("peak_hold", {"decay_rate": 0.0})
        a = _make_analysis(rms=0.9)
        f.process(a)
        f.reset()
        # After reset, processing a low value should return the low value
        a2 = _make_analysis(rms=0.1, peak=0.1)
        r2 = f.process(a2)
        assert r2.rms == pytest.approx(0.1)


# ---------------------------------------------------------------------------
# AudioFilterPipeline
# ---------------------------------------------------------------------------


class TestAudioFilterPipeline:
    def test_empty_pipeline_passthrough(self):
        pipeline = AudioFilterPipeline([])
        assert pipeline.empty is True
        a = _make_analysis()
        result = pipeline.process(a)
        assert result is a

    def test_single_filter(self):
        pipeline = AudioFilterPipeline(
            [
                FilterInstance("gain", {"factor": 2.0}),
            ]
        )
        assert pipeline.empty is False
        a = _make_analysis(rms=0.3, peak=0.4)
        result = pipeline.process(a)
        assert pytest.approx(result.rms) == 0.6

    def test_chained_filters(self):
        """Gain 2x then invert: rms=0.3 -> 0.6 -> 0.4."""
        pipeline = AudioFilterPipeline(
            [
                FilterInstance("gain", {"factor": 2.0}),
                FilterInstance("inverter", {"invert_spectrum": False}),
            ]
        )
        a = _make_analysis(rms=0.3, peak=0.4)
        result = pipeline.process(a)
        assert pytest.approx(result.rms, abs=1e-6) == 0.4

    def test_unknown_filter_skipped(self):
        """Unknown filters are silently skipped, remaining filters still work."""
        pipeline = AudioFilterPipeline(
            [
                FilterInstance("nonexistent_filter", {}),
                FilterInstance("gain", {"factor": 2.0}),
            ]
        )
        a = _make_analysis(rms=0.3)
        result = pipeline.process(a)
        assert pytest.approx(result.rms) == 0.6

    def test_reset_resets_stateful_filters(self):
        pipeline = AudioFilterPipeline(
            [
                FilterInstance("peak_hold", {"decay_rate": 0.0}),
            ]
        )
        a = _make_analysis(rms=0.9)
        pipeline.process(a)
        pipeline.reset()
        a2 = _make_analysis(rms=0.1, peak=0.1)
        result = pipeline.process(a2)
        assert result.rms == pytest.approx(0.1)

    def test_close_clears_filters(self):
        pipeline = AudioFilterPipeline(
            [
                FilterInstance("gain", {"factor": 2.0}),
            ]
        )
        assert not pipeline.empty
        pipeline.close()
        assert pipeline.empty
