from __future__ import annotations

import asyncio
import json
import math
import wave
from pathlib import Path

from app.tools.local_stresstest.audio import (
    AudioSample,
    audio_metrics,
    build_variations,
    read_wav_mono16,
    vad_proxy_metrics,
    write_wav,
)
from app.tools.local_stresstest.report import render_markdown_report, write_csv, write_mermaid_files
from app.tools.local_stresstest.runner import StressConfig, wait_for_local_services
from app.tools.local_stresstest.scenarios import scenario_description
from app.tools.local_stresstest.text import compare_text, normalize_text, word_error_rate
from app.tools.local_stresstest.timeline import excerpt_timeline, read_timeline, timeline_has_error


def _tone_pcm(*, sample_rate: int = 16_000, duration_ms: int = 300, hz: float = 440.0) -> bytes:
    samples = round(sample_rate * duration_ms / 1000)
    out = bytearray()
    for idx in range(samples):
        value = round(math.sin(2 * math.pi * hz * idx / sample_rate) * 9000)
        out.extend(int(value).to_bytes(2, byteorder="little", signed=True))
    return bytes(out)


def test_normalize_text_removes_accents_case_and_punctuation() -> None:
    assert normalize_text("  Teste UNITÁRIO, Sofya!  ") == "teste unitario sofya"


def test_word_error_rate_counts_insertions_deletions_and_substitutions() -> None:
    wer, substitutions, deletions, insertions = word_error_rate(
        "teste unitario do stt sofya",
        "teste unitario stt sofia agora",
    )

    assert round(wer, 2) == 0.6
    assert substitutions == 1
    assert deletions == 1
    assert insertions == 1


def test_compare_text_reports_missing_critical_terms() -> None:
    comparison = compare_text(
        expected="Teste unitario do STT Sofya",
        actual="Teste unitario do STT",
        critical_terms=["teste", "sofya"],
    )

    assert comparison.terms_ok is False
    assert comparison.missing_terms == ("sofya",)


def test_audio_variations_keep_expected_names_and_are_nonempty() -> None:
    sample = AudioSample(name="base", pcm=_tone_pcm())
    variations = build_variations(sample)

    assert [item.name for item in variations] == [
        "clean",
        "low_volume",
        "very_low_volume",
        "high_volume",
        "clipped_high_volume",
        "leading_trailing_silence",
        "short_leading_silence",
        "long_leading_silence",
        "noise_snr_20",
        "noise_snr_15",
        "noise_snr_10",
        "pre_noise_300ms",
        "pre_noise_800ms",
        "telephony_profile",
        "telephony_low_volume",
        "initial_fade_in_250ms",
        "initial_fade_in_500ms",
        "initial_fade_in_900ms",
        "initial_dip_300ms",
        "initial_dip_700ms",
        "prefix_300ms_15pct",
        "prefix_600ms_20pct",
        "prefix_900ms_25pct",
        "low_prefix_noise_600ms",
        "low_prefix_telephony_700ms",
    ]
    assert all(item.pcm for item in variations)
    assert len(variations[5].pcm) > len(sample.pcm)
    assert len(variations) == 25


def test_audio_metrics_detects_audible_tone_and_duration() -> None:
    metrics = audio_metrics(_tone_pcm(duration_ms=500))

    assert metrics.duration_ms == 500
    assert metrics.audible is True
    assert metrics.clipping_ratio == 0.0


def test_vad_proxy_metrics_flags_soft_start_risk() -> None:
    sample = AudioSample(name="base", pcm=(b"\x00" * 1600) + _tone_pcm(duration_ms=300))
    metrics = vad_proxy_metrics(sample, threshold_dbfs=-45.0, prefix_padding_ms=20)

    assert metrics.first_voice_ms > 0
    assert metrics.unrecovered_prefix_ms > 0
    assert metrics.low_start_risk is True


def test_wav_read_converts_to_16k_mono(tmp_path: Path) -> None:
    stereo_path = tmp_path / "stereo.wav"
    left = _tone_pcm(sample_rate=8_000, duration_ms=100)
    stereo = bytearray()
    for idx in range(0, len(left), 2):
        stereo.extend(left[idx : idx + 2])
        stereo.extend(left[idx : idx + 2])
    with wave.open(str(stereo_path), "wb") as handle:
        handle.setnchannels(2)
        handle.setsampwidth(2)
        handle.setframerate(8_000)
        handle.writeframes(bytes(stereo))

    sample = read_wav_mono16(stereo_path)

    assert sample.sample_rate == 16_000
    assert sample.channels == 1
    assert audio_metrics(sample.pcm).duration_ms == 100


def test_write_wav_creates_parent_directory(tmp_path: Path) -> None:
    out = write_wav(tmp_path / "nested" / "tone.wav", AudioSample(name="tone", pcm=_tone_pcm()))

    assert out.exists()
    assert read_wav_mono16(out).pcm


def test_render_markdown_report_contains_tables_and_mermaid() -> None:
    report = render_markdown_report(
        {
            "passed": True,
            "started_at": "2026-06-16T00:00:00Z",
            "duration_ms": 123,
            "expected_text": "Teste",
            "baseline": {"path": "baseline.wav", "synthetic": True},
            "stt_results": [{"scenario": "clean", "passed": True, "wer": 0.0}],
            "tts_results": [{"scenario": "short", "passed": True, "duration_ms": 1000}],
            "e2e_results": [{"scenario": "clean", "passed": True, "ready_received": True}],
            "startup_checks": {
                "bridge": {
                    "service": "bridge",
                    "url": "http://127.0.0.1:8000/health",
                    "status": "ready",
                    "attempts": 1,
                    "detail": "HTTP 200",
                }
            },
            "artifacts": {"summary_json": "summary.json"},
        }
    )

    assert "Aviso: esta execucao usou um audio base sintetico" in report
    assert "## Prontidao Local" in report
    assert "Versao textual:" in report
    assert "![Fluxo STT](diagrams/stt_flow.svg)" in report
    assert "Codigo Mermaid:" in report
    assert "```mermaid" in report
    assert "| cenario | descricao | status | prefixo_ok |" in report
    assert "Audio base sem degradacao" in report


def test_scenario_description_ignores_repeat_suffix() -> None:
    assert scenario_description("noise_snr_20_r2").startswith("Audio com ruido leve")


def test_write_mermaid_files_creates_standalone_diagrams(tmp_path: Path) -> None:
    files = write_mermaid_files(tmp_path)

    assert Path(files["stt_mermaid"]).read_text(encoding="utf-8").startswith("flowchart LR")
    assert "sequenceDiagram" in Path(files["e2e_mermaid"]).read_text(encoding="utf-8")
    assert Path(files["stt_svg"]).read_text(encoding="utf-8").startswith("<svg")


def _stress_config(tmp_path: Path) -> StressConfig:
    return StressConfig(
        env_file=Path(".env.dev"),
        report_dir=tmp_path,
        expected_text="Teste",
        synthesis_text="Teste",
        critical_terms=("teste",),
        stt_wer_threshold=0.2,
        bridge_url="ws://127.0.0.1:8000/ws/agent",
        bridge_health_url="http://bridge.local/health",
        agent_health_url="http://agent.local/",
        startup_wait_s=5.0,
        startup_poll_s=0.01,
        skip_local_wait=False,
        repeat=1,
        concurrency=1,
        e2e_turns=1,
        e2e_timeout_s=5.0,
        stress_audio=None,
        prefix_text="Teste",
        prefix_words=1,
        vad_proxy_threshold_dbfs=-45.0,
        vad_proxy_prefix_padding_ms=1000,
        vad_proxy_min_speech_ms=100,
    )


def test_wait_for_local_services_retries_until_agent_is_ready(tmp_path: Path) -> None:
    config = _stress_config(tmp_path)
    attempts: dict[str, int] = {}

    async def probe(url: str) -> tuple[bool, str]:
        attempts[url] = attempts.get(url, 0) + 1
        if "agent" in url and attempts[url] < 3:
            return False, "connection refused"
        return True, "HTTP 200"

    async def no_sleep(_: float) -> None:
        return None

    states = asyncio.run(wait_for_local_services(config, probe=probe, sleep=no_sleep))

    assert states["bridge"]["ok"] is True
    assert states["bridge"]["attempts"] == 1
    assert states["agent_runtime"]["ok"] is True
    assert states["agent_runtime"]["attempts"] == 3


def test_write_csv_handles_empty_rows(tmp_path: Path) -> None:
    out = write_csv(tmp_path / "empty.csv", [])

    assert out.read_text(encoding="utf-8").strip() == ""


def test_timeline_helpers_parse_excerpt_and_errors(tmp_path: Path) -> None:
    path = tmp_path / "timeline.jsonl"
    records = [
        {"event": "ready_sent"},
        {"event": "noise"},
        {"event": "user_transcript_final", "text": "ola"},
        {"event": "bridge_failed"},
    ]
    path.write_text("\n".join(json.dumps(item) for item in records), encoding="utf-8")

    parsed = read_timeline(path)

    assert parsed == records
    assert [item["event"] for item in excerpt_timeline(parsed)] == [
        "ready_sent",
        "user_transcript_final",
        "bridge_failed",
    ]
    assert timeline_has_error(parsed) is True
