from __future__ import annotations

import copy

import numpy as np

from engine.preset_adapter import adapt_preset_for_audio, analyze_source
from engine.presets import GENRE_PRESETS


EXPECTED_MODULE_ORDER = [
    "mono_bass", "dereverb", "hf_restoration", "dynamic_eq", "deesser",
    "multiband", "vintage", "harmonic_enhancement", "modern", "ai_dehaze",
    "transient_shaper", "stereo_imaging", "auto_sidechain", "final_limiting",
]


def _stereo_signal(*, sr=44100, seconds=3.0, dull=False, sibilant=False, transient=False):
    rng = np.random.default_rng(20260824)
    n = int(sr * seconds)
    t = np.arange(n, dtype=np.float64) / sr
    left = 0.12 * np.sin(2 * np.pi * 110 * t) + 0.05 * np.sin(2 * np.pi * 900 * t)
    right = 0.115 * np.sin(2 * np.pi * 111 * t + 0.05) + 0.045 * np.sin(2 * np.pi * 920 * t)
    # A small, independent wide-band bed gives healthy air/stereo information.
    left += rng.normal(0, 0.006, n)
    right += rng.normal(0, 0.006, n)
    if sibilant:
        left += 0.12 * np.sin(2 * np.pi * 6200 * t)
        right += 0.115 * np.sin(2 * np.pi * 6200 * t + 0.03)
    if transient:
        for start in range(0, n, sr // 2):
            width = min(160, n - start)
            if width:
                pulse = np.linspace(0.8, 0.0, width)
                left[start:start + width] += pulse
                right[start:start + width] += pulse
    audio = np.column_stack((left, right))
    if dull:
        from scipy import signal
        sos = signal.butter(6, 7000 / (sr / 2), btype="low", output="sos")
        audio = signal.sosfiltfilt(sos, audio, axis=0)
    peak = np.max(np.abs(audio))
    if peak > 0.9:
        audio *= 0.9 / peak
    return audio.astype(np.float32), sr


def test_all_visible_builtins_are_resolved_v2_profiles():
    assert len(GENRE_PRESETS) == 50
    assert "_Families" not in GENRE_PRESETS
    for name, preset in GENRE_PRESETS.items():
        assert preset["preset_version"] == 2, name
        assert preset["family"], name
        assert preset["module_order"] == EXPECTED_MODULE_ORDER, name
        assert set(preset["active_modules"]) == set(EXPECTED_MODULE_ORDER), name
        lo, hi = preset["intent"]["lufs_range"]
        assert lo <= preset["target_lufs"] <= hi, name


def test_reference_backed_genres_use_measured_targets():
    expected = {
        "Psytrance": (-10.1, -10.090156345187884, 0.0083, 0.1876, 0.1560),
        "Tech House": (-9.6, -9.611815865246276, 0.0235, 0.2033, 0.1450),
        "Melodic Techno": (-8.8, -8.7820201196041, 0.0150, 0.2271, 0.0922),
    }
    for name, (target, reference_lufs, low, mid, high) in expected.items():
        preset = GENRE_PRESETS[name]
        intent = preset["intent"]
        assert preset["target_lufs"] == target
        assert intent["reference_backed"] is True
        assert intent["reference_lufs"] == reference_lufs
        assert intent["low_side_ratio"] == low
        assert intent["mid_side_ratio"] == mid
        assert intent["high_side_ratio"] == high


def test_v2_uses_full_engine_as_eligible_corrective_palette():
    coverage = {module: 0 for module in EXPECTED_MODULE_ORDER}
    for preset in GENRE_PRESETS.values():
        for module, enabled in preset["active_modules"].items():
            coverage[module] += int(bool(enabled))
    for module in (
        "dynamic_eq", "deesser", "multiband", "stereo_imaging",
        "hf_restoration", "harmonic_enhancement", "modern", "vintage",
    ):
        assert coverage[module] > 0, module
    # De-reverb is intentionally not a genre prior: it should be enabled only
    # by explicit user choice/analysis of an actually over-wet source.
    assert coverage["dereverb"] == 0


def test_adaptation_never_enables_user_disabled_modules():
    audio, sr = _stereo_signal(dull=True, sibilant=True, transient=True)
    preset = GENRE_PRESETS["Psytrance"]
    active = {key: False for key in preset["active_modules"]}
    effective, adapted, _ = adapt_preset_for_audio(audio, sr, preset, active)
    assert not any(adapted.values())
    assert effective["preset_version"] == 2


def test_adaptation_does_not_mutate_input_dicts():
    audio, sr = _stereo_signal(dull=True)
    preset = copy.deepcopy(GENRE_PRESETS["Tech House"])
    active = copy.deepcopy(preset["active_modules"])
    preset_before = copy.deepcopy(preset)
    active_before = copy.deepcopy(active)
    adapt_preset_for_audio(audio, sr, preset, active)
    assert preset == preset_before
    assert active == active_before


def test_dull_source_selects_single_hf_repair_path():
    audio, sr = _stereo_signal(dull=True)
    preset = GENRE_PRESETS["Tech House"]
    effective, active, features = adapt_preset_for_audio(
        audio, sr, preset, preset["active_modules"]
    )
    assert features["air_rel_db"] <= preset["adaptive"]["air_severe_db"]
    assert active["hf_restoration"] is True
    assert effective["hf_restoration_mode"] == "dsp_fast"
    assert active["ai_dehaze"] is False


def test_strong_4_to_8k_content_keeps_gentle_deesser_eligible():
    audio, sr = _stereo_signal(sibilant=True)
    preset = GENRE_PRESETS["Melodic Techno"]
    effective, active, features = adapt_preset_for_audio(
        audio, sr, preset, preset["active_modules"]
    )
    assert features["sibilance_rel_db"] >= preset["adaptive"]["sibilance_trigger_db"]
    assert active["deesser"] is True
    assert 2.0 <= effective["deesser_ratio"] <= 3.5


def test_locked_user_toggle_prevents_adaptive_bypass():
    audio, sr = _stereo_signal()
    preset = GENRE_PRESETS["Tech House"]
    active = dict(preset["active_modules"])
    active["deesser"] = True
    _, adapted, _ = adapt_preset_for_audio(
        audio, sr, preset, active, locked_modules={"deesser"}
    )
    assert adapted["deesser"] is True


def test_reference_features_are_finite_for_normal_audio():
    audio, sr = _stereo_signal(transient=True)
    features = analyze_source(audio, sr)
    assert set(features) >= {
        "crest_db", "low_mid_rel_db", "high_mid_rel_db", "sibilance_rel_db",
        "air_rel_db", "low_side_ratio", "mid_side_ratio", "high_side_ratio",
    }
    assert all(np.isfinite(value) for value in features.values())


def test_heavy_modern_chain_is_not_a_whole_family_default():
    assert GENRE_PRESETS["Melodic Techno"]["active_modules"]["modern"] is False
    assert GENRE_PRESETS["Liquid DnB"]["active_modules"]["modern"] is False
    for name in ("Peak Time Techno", "Industrial Techno", "Neurofunk", "Hardstyle"):
        assert GENRE_PRESETS[name]["active_modules"]["modern"] is True, name


def test_reference_backed_presets_carry_true_peak_ceiling_and_eq_anchors():
    for name in ("Psytrance", "Tech House", "Melodic Techno"):
        assert GENRE_PRESETS[name]["output_ceiling_db"] == -1.0
    assert len(GENRE_PRESETS["Psytrance"].get("reference_eq_bands", [])) == 4
    assert len(GENRE_PRESETS["Melodic Techno"].get("reference_eq_bands", [])) == 2


def test_reference_backed_width_can_reach_measured_narrow_or_wide_targets():
    audio, sr = _stereo_signal()
    preset = copy.deepcopy(GENRE_PRESETS["Tech House"])
    # Force a measured-reference correction path and verify it is not held to
    # the conservative family-only 1.30x ceiling.
    preset["intent"]["mid_side_ratio"] = 0.35
    preset["intent"]["high_side_ratio"] = 0.35
    effective, active, _ = adapt_preset_for_audio(
        audio, sr, preset, preset["active_modules"]
    )
    assert active["stereo_imaging"] is True
    assert effective["stereo_target_ratios"] == {
        "low": preset["intent"]["low_side_ratio"],
        "mid": 0.35,
        "high": 0.35,
    }


def test_psytrance_keeps_measured_width_intent_separate_from_prelimit_compensation():
    preset = GENRE_PRESETS["Psytrance"]
    assert preset["intent"]["high_side_ratio"] == 0.156
    assert preset["stereo_match_ratios"]["high"] == 0.106
