from __future__ import annotations

import logging

from app.utils import background as background_module
from app.utils.background import (
    AudioOutputBacklogConfig,
    AudioOutputLatencyTracker,
    audio_output_backlog_config_from_env,
)

_LOGGER = logging.getLogger("test.audio_out")


def _capture(monkeypatch) -> list:
    events: list = []
    monkeypatch.setattr(
        background_module,
        "log_flow_event",
        lambda logger, step, **payload: events.append((step, payload)),
    )
    return events


def test_output_config_from_env_defaults(monkeypatch) -> None:
    for name in (
        "AUDIO_OUT_LATENCY_METRICS_ENABLED",
        "AUDIO_OUT_LATENCY_ALERT_MS",
        "AUDIO_OUT_LATENCY_LOG_INTERVAL_S",
        "AUDIO_OUT_PACING_DEBT_THRESHOLD_MS",
        "AUDIO_OUT_BACKLOG_SHED_ENABLED",
        "AUDIO_OUT_BACKLOG_SHED_THRESHOLD_MS",
        "AUDIO_OUT_BACKLOG_SHED_KEEP_MS",
        "AUDIO_OUT_BACKLOG_SHED_CHECK_INTERVAL_FRAMES",
        "AUDIO_OUT_BACKLOG_SILENCE_DBFS",
    ):
        monkeypatch.delenv(name, raising=False)

    config = audio_output_backlog_config_from_env()

    assert config.metrics_enabled is True
    assert config.shed_enabled is True
    assert config.shed_threshold_ms == 400
    assert config.shed_keep_ms == 200
    assert config.pacing_debt_threshold_ms == 200
    assert config.silence_dbfs == -60.0
    # frames dependem do frame_ms passado no uso
    assert config.shed_threshold_frames(20) == 20
    assert config.shed_keep_frames(20) == 10
    assert config.silence_rms_threshold == 32


def test_note_pacing_debt_accumulates_and_logs(monkeypatch) -> None:
    events = _capture(monkeypatch)
    tracker = AudioOutputLatencyTracker(
        AudioOutputBacklogConfig(), frame_ms=20, flow_logger=_LOGGER
    )

    tracker.note_pacing_debt(250, now=1.0)
    tracker.note_pacing_debt(300, now=2.0)
    tracker.note_pacing_debt(0, now=3.0)  # ignora nao-positivo

    assert tracker.pacing_debt_events == 2
    assert tracker.pacing_debt_dropped_ms == 550
    debt_events = [e for e in events if e[0] == "audio_out_pacing_debt"]
    assert len(debt_events) == 2
    assert debt_events[-1][1]["pacing_debt_dropped_ms"] == 550
    assert debt_events[-1][1]["debt_ms"] == 300


def test_record_shed_accumulates_and_logs(monkeypatch) -> None:
    events = _capture(monkeypatch)
    debug_events = []
    tracker = AudioOutputLatencyTracker(
        AudioOutputBacklogConfig(),
        frame_ms=20,
        flow_logger=_LOGGER,
        debug_event_publisher=lambda event, payload: debug_events.append(
            (event, payload)
        ),
    )

    tracker.record_shed(
        dropped_frames=3,
        dropped_silent=3,
        mode="energy",
        queue_before_frames=30,
        queue_after_frames=27,
    )
    tracker.record_shed(
        dropped_frames=0,  # ignora
        dropped_silent=0,
        mode="energy",
        queue_before_frames=10,
        queue_after_frames=10,
    )

    assert tracker.total_dropped_frames == 3
    shed_events = [e for e in events if e[0] == "audio_out_latency_shed"]
    assert len(shed_events) == 1
    payload = shed_events[0][1]
    assert payload["mode"] == "energy"
    assert payload["dropped_ms"] == 60
    assert payload["dropped_silent_frames"] == 3
    assert payload["queue_before_ms"] == 600
    assert payload["total_dropped_ms"] == 60
    assert debug_events[-1][0] == "bridge.audio_out.shed"
    assert debug_events[-1][1]["dropped_ms"] == 60


def test_maybe_log_primes_first_then_logs_on_interval(monkeypatch) -> None:
    events = _capture(monkeypatch)
    config = AudioOutputBacklogConfig(latency_alert_ms=1000, latency_log_interval_s=15.0)
    tracker = AudioOutputLatencyTracker(config, frame_ms=20, flow_logger=_LOGGER)

    # primeira chamada apenas inicializa o relogio (sem log)
    tracker.maybe_log(queue_frames=5, now=100.0, reason="tick")
    assert [e for e in events if e[0] == "audio_out_latency"] == []
    assert tracker.peak_queue_ms == 100  # peak atualiza mesmo sem logar

    # antes do intervalo: ainda nao loga
    tracker.maybe_log(queue_frames=5, now=110.0, reason="tick")
    assert [e for e in events if e[0] == "audio_out_latency"] == []

    # passado o intervalo: loga
    tracker.maybe_log(queue_frames=5, now=116.0, reason="tick")
    out_events = [e for e in events if e[0] == "audio_out_latency"]
    assert len(out_events) == 1
    assert out_events[0][1]["queue_ms"] == 100


def test_maybe_log_logs_on_alert_threshold(monkeypatch) -> None:
    events = _capture(monkeypatch)
    config = AudioOutputBacklogConfig(latency_alert_ms=1000, latency_log_interval_s=999.0)
    tracker = AudioOutputLatencyTracker(config, frame_ms=20, flow_logger=_LOGGER)

    tracker.maybe_log(queue_frames=5, now=1.0, reason="tick")  # prime
    # backlog >= alert (60 frames * 20 = 1200 ms >= 1000) -> loga fora do intervalo
    tracker.maybe_log(queue_frames=60, now=1.5, reason="tick")

    out_events = [e for e in events if e[0] == "audio_out_latency"]
    assert len(out_events) == 1
    assert out_events[0][1]["queue_ms"] == 1200
    assert out_events[0][1]["peak_queue_ms"] == 1200


def test_metrics_disabled_suppresses_all_logs(monkeypatch) -> None:
    events = _capture(monkeypatch)
    config = AudioOutputBacklogConfig(metrics_enabled=False)
    tracker = AudioOutputLatencyTracker(config, frame_ms=20, flow_logger=_LOGGER)

    tracker.note_pacing_debt(500, now=1.0)
    tracker.record_shed(
        dropped_frames=5, dropped_silent=5, mode="energy",
        queue_before_frames=40, queue_after_frames=35,
    )
    tracker.maybe_log(queue_frames=100, now=1.0, reason="tick", force=True)

    assert events == []
    # contadores ainda acumulam (metrica desligada nao perde estado interno)
    assert tracker.pacing_debt_dropped_ms == 500
    assert tracker.total_dropped_frames == 5


def test_maybe_log_alert_is_rate_limited_until_it_recovers(monkeypatch) -> None:
    events = _capture(monkeypatch)
    config = AudioOutputBacklogConfig(latency_alert_ms=1000, latency_log_interval_s=15.0)
    tracker = AudioOutputLatencyTracker(config, frame_ms=20, flow_logger=_LOGGER)

    tracker.maybe_log(queue_frames=5, now=1.0, reason="tick")
    tracker.maybe_log(queue_frames=60, now=1.5, reason="alert_start")
    tracker.maybe_log(queue_frames=60, now=1.52, reason="alert_still_active")
    tracker.maybe_log(queue_frames=5, now=2.0, reason="recovered")
    tracker.maybe_log(queue_frames=60, now=2.1, reason="alert_restart")

    out_events = [event for event in events if event[0] == "audio_out_latency"]
    assert len(out_events) == 2
    assert [event[1]["reason"] for event in out_events] == ["alert_start", "alert_restart"]
