"""Tests for biquad.py (Audio EQ Cookbook designs + DF-II-Transposed processing).

Lives under basics/ and was previously uncovered (testpaths=topics). Pins the
design magnitude responses and the processing recurrence against scipy.
"""

import numpy as np
import pytest
from scipy.signal import sosfreqz, sosfilt

from biquad import (
    design_peaking_eq, design_low_shelf, design_high_shelf,
    design_parametric_eq, quantize_sos, biquad_process,
)

FS = 48000.0


def _gain_db_at(sos, freq, fs=FS):
    """Magnitude response in dB at a single frequency."""
    w, h = sosfreqz(sos, worN=[2 * np.pi * freq / fs])
    return 20 * np.log10(np.abs(h[0]))


# ----------------------------------------------------------------- peaking EQ

def test_peaking_eq_gain_at_center():
    sos = design_peaking_eq(fc=1000, gain_db=6.0, Q=2.0, fs=FS)
    assert np.isclose(_gain_db_at(sos, 1000), 6.0, atol=0.1)


def test_peaking_eq_unity_far_from_center():
    sos = design_peaking_eq(fc=1000, gain_db=6.0, Q=2.0, fs=FS)
    assert abs(_gain_db_at(sos, 50)) < 0.5         # ~0 dB well below fc
    assert abs(_gain_db_at(sos, 18000)) < 0.5      # ~0 dB well above fc


def test_peaking_eq_cut():
    sos = design_peaking_eq(fc=2000, gain_db=-9.0, Q=1.0, fs=FS)
    assert np.isclose(_gain_db_at(sos, 2000), -9.0, atol=0.1)


# ----------------------------------------------------------------- shelves

def test_low_shelf_dc_and_hf():
    sos = design_low_shelf(fc=200, gain_db=6.0, Q=0.707, fs=FS)
    assert np.isclose(_gain_db_at(sos, 10), 6.0, atol=0.3)    # boosted at DC
    assert abs(_gain_db_at(sos, 18000)) < 0.5                 # unity up high


def test_high_shelf_dc_and_hf():
    sos = design_high_shelf(fc=5000, gain_db=6.0, Q=0.707, fs=FS)
    assert abs(_gain_db_at(sos, 20)) < 0.5                    # unity at DC
    assert np.isclose(_gain_db_at(sos, 23000), 6.0, atol=0.3)  # boosted up high


# --------------------------------------------------------------- parametric EQ

def test_parametric_eq_stacks_sections():
    bands = [(100, 3, 0.7, "lowshelf"),
             (1000, -4, 2.0, "peak"),
             (8000, 5, 0.7, "highshelf")]
    sos = design_parametric_eq(bands, FS)
    assert sos.shape == (3, 6)


def test_parametric_eq_rejects_unknown_type():
    with pytest.raises(ValueError):
        design_parametric_eq([(1000, 3, 1.0, "notch")], FS)


# ----------------------------------------------------------------- quantize

def test_quantize_leaves_a0_column():
    sos = design_peaking_eq(1000, 6, 2, FS)
    q = quantize_sos(sos, bits=16)
    assert np.all(q[:, 3] == 1.0)                  # the a0 = 1 column untouched


def test_quantize_snaps_to_grid():
    sos = design_peaking_eq(1000, 6, 2, FS)
    q = quantize_sos(sos, bits=8)
    scale = 2 ** 7
    for col in (0, 1, 2, 4, 5):
        # every quantised coefficient is an integer multiple of 1/scale
        assert np.allclose(q[:, col] * scale, np.round(q[:, col] * scale))


def test_quantize_clips_range():
    sos = design_peaking_eq(1000, 6, 2, FS)
    q = quantize_sos(sos, bits=16)
    assert q.min() >= -1.0 and q.max() <= 1.0


# ----------------------------------------------------------------- processing

def test_biquad_process_matches_sosfilt():
    sos = design_parametric_eq(
        [(200, 4, 0.7, "lowshelf"), (1500, -6, 2.0, "peak")], FS)
    x = np.random.default_rng(0).standard_normal(2000)
    assert np.allclose(biquad_process(sos, x), sosfilt(sos, x), atol=1e-9)


def test_biquad_process_impulse_starts_at_b0():
    sos = design_peaking_eq(1000, 6, 2, FS)
    x = np.zeros(16); x[0] = 1.0
    y = biquad_process(sos, x)
    assert np.isclose(y[0], sos[0, 0])             # first sample = b0


def test_biquad_process_unity_passthrough():
    # an all-pass-through cascade (b0=1, rest 0) returns the input
    sos = np.array([[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]])
    x = np.random.default_rng(1).standard_normal(100)
    assert np.allclose(biquad_process(sos, x), x)
