from __future__ import annotations

import struct

import numpy as np

from app.livekit.adapters.audio_gain import (
    GainEmitter,
    SoftClipGain,
    tts_output_gain_from_env,
)


def _pcm(*samples: int) -> bytes:
    return struct.pack("<" + "h" * len(samples), *samples)


def _samples(pcm: bytes) -> list[int]:
    return list(struct.unpack("<" + "h" * (len(pcm) // 2), pcm))


def test_gain_1_0_is_disabled_and_passthrough() -> None:
    g = SoftClipGain(gain=1.0)
    assert g.enabled is False
    pcm = _pcm(1000, -2000, 3000)
    assert g.process(pcm) == pcm


def test_normal_level_is_boosted_near_linear() -> None:
    g = SoftClipGain(gain=2.0)  # ceiling default -1 dBFS
    # sinal baixo (~ -30 dBFS): boost deve ser praticamente 2x
    out = _samples(g.process(_pcm(1000, -1000)))
    assert abs(out[0] - 2000) <= 40
    assert abs(out[1] + 2000) <= 40


def test_hot_peaks_never_clip_past_ceiling() -> None:
    ceiling = 0.891  # ~ -1 dBFS
    g = SoftClipGain(gain=2.0, ceiling=ceiling)
    limit = int(ceiling * 32768) + 1
    # picos quentes que, com 2x linear, estourariam o fundo de escala
    out = _samples(g.process(_pcm(30000, -30000, 25000, -25000)))
    assert all(abs(s) <= limit for s in out), out
    # e continua monotonicamente crescente (sem wraparound/inversao de fase)
    assert out[0] > 0 and out[1] < 0


def test_monotonic_transfer_curve() -> None:
    g = SoftClipGain(gain=2.0)
    xs = list(range(0, 32000, 1000))
    ys = [_samples(g.process(_pcm(x)))[0] for x in xs]
    assert all(b >= a for a, b in zip(ys, ys[1:])), ys


def test_odd_length_bytes_do_not_crash() -> None:
    g = SoftClipGain(gain=2.0)
    pcm = _pcm(1000, -1000) + b"\x7f"  # 1 byte solto
    out = g.process(pcm)
    assert len(out) == len(pcm)
    assert out[-1:] == b"\x7f"


def test_empty_input() -> None:
    assert SoftClipGain(gain=2.0).process(b"") == b""


def test_env_loader_defaults_to_disabled(monkeypatch) -> None:
    monkeypatch.delenv("TTS_OUTPUT_GAIN", raising=False)
    monkeypatch.delenv("TTS_OUTPUT_CEILING_DBFS", raising=False)
    g = tts_output_gain_from_env()
    assert g.gain == 1.0
    assert g.enabled is False


def test_env_loader_reads_gain_and_ceiling(monkeypatch) -> None:
    monkeypatch.setenv("TTS_OUTPUT_GAIN", "2.0")
    monkeypatch.setenv("TTS_OUTPUT_CEILING_DBFS", "-6")
    g = tts_output_gain_from_env()
    assert g.gain == 2.0
    assert abs(g.ceiling - 10 ** (-6 / 20.0)) < 1e-6


class _FakeEmitter:
    def __init__(self) -> None:
        self.pushed: list[bytes] = []
        self.initialized = False
        self.flushed = False

    def initialize(self, **kwargs) -> None:
        self.initialized = True

    def push(self, data: bytes) -> None:
        self.pushed.append(data)

    def flush(self) -> None:
        self.flushed = True


def test_gain_emitter_transforms_push_and_forwards_rest() -> None:
    inner = _FakeEmitter()
    em = GainEmitter(inner, SoftClipGain(gain=2.0))

    em.initialize(sample_rate=24000)
    em.push(_pcm(1000, -1000))
    em.flush()

    assert inner.initialized is True
    assert inner.flushed is True
    assert len(inner.pushed) == 1
    # o que chegou ao emitter real foi amplificado
    out = _samples(inner.pushed[0])
    assert abs(out[0] - 2000) <= 40


def test_gain_emitter_matches_numpy_reference() -> None:
    gain = SoftClipGain(gain=2.0, ceiling=0.891)
    pcm = _pcm(500, -12000, 28000, -31000, 0)
    ref_x = np.frombuffer(pcm, dtype="<i2").astype(np.float32) / 32768.0
    ref_y = 0.891 * np.tanh((2.0 / 0.891) * ref_x)
    ref = np.clip(np.rint(ref_y * 32768.0), -32768.0, 32767.0).astype("<i2")
    assert gain.process(pcm) == ref.tobytes()


def test_gain_configuration_rejects_non_finite_and_out_of_range_values(monkeypatch) -> None:
    monkeypatch.setenv("TTS_OUTPUT_GAIN", "nan")
    monkeypatch.setenv("TTS_OUTPUT_CEILING_DBFS", "inf")
    gain = tts_output_gain_from_env()
    assert gain.gain == 1.0

    monkeypatch.setenv("TTS_OUTPUT_GAIN", "-2")
    monkeypatch.setenv("TTS_OUTPUT_CEILING_DBFS", "-200")
    gain = tts_output_gain_from_env()
    assert gain.ceiling == 0.05
    assert gain.gain == 0.0
