"""Full-track streaming (#7) — the guarantees that make big uploads safe.

Proves the two things unit tests on 2 s clips can't: (1) the signal/loudness
engines now analyse the WHOLE track, past the old 60 s / 180 s head cap, and
(2) streaming didn't change the numbers — content_id stays byte-identical to the
old whole-file hash (so the deployed cache stays warm), and exact reductions
(waveform min/max) equal a single-pass computation.

    cd armada/api && python -m pytest tests/test_fulltrack.py -q
"""
import hashlib
import tempfile
from pathlib import Path

import numpy as np
import soundfile as sf

import cache
from engines import loudness, signal, streaming

SR = 22050


def _click_track(bpm=120, secs=70.0, sr=SR):
    """A long pulse train (>60 s) so a head-only analysis would visibly truncate."""
    n = int(sr * secs)
    y = np.zeros(n, dtype="float32")
    step = int(sr * 60 / bpm)
    for i in range(0, n, step):
        y[i:i + 200] += np.hanning(min(200, n - i)).astype("float32")
    return y


def _write(y, sr=SR, suffix=".wav"):
    p = Path(tempfile.mktemp(suffix=suffix))
    sf.write(str(p), y, sr)
    return str(p)


# ── (1) full-track coverage: past the old 60 s / 180 s head cap ─────────────────
def test_onsets_cover_whole_track_not_just_head():
    p = _write(_click_track(secs=70.0))
    r = signal.onsets(p)
    assert r["full_track"] is True
    assert r["duration_s"] >= 69.0                 # whole track, not capped at 60
    assert max(r["onsets"]) > 60.0                 # hits found beyond the old head window
    assert r["n_onsets"] >= 120                     # ~120 bpm × 70 s ≈ 140 pulses


def test_waveform_spans_whole_track():
    p = _write(_click_track(secs=70.0))
    r = signal.waveform(p, bins=100)
    assert r["full_track"] is True and r["bins"] == 100
    assert r["duration_s"] >= 69.0
    # pulses recur throughout → energy present in the last bins, not only the first
    assert r["rms"][-1] > 0.0 and r["rms"][90] > 0.0


def test_loudness_full_track_exact_peak_and_flagged_approx():
    y = _click_track(secs=70.0) * 0.5
    r = loudness.loudness(_write(y))
    assert r["full_track"] is True and r["approximate"] is True
    assert r["lufs_method"] == "chunked" and r["lufs_integrated"] is not None
    # peak is an EXACT running max over the stream
    assert abs(r["peak_dbfs"] - 20 * np.log10(float(np.max(np.abs(y))))) < 0.1
    assert r["duration_s"] >= 69.0


# ── (2) streaming didn't change the numbers ─────────────────────────────────────
def test_content_id_streaming_equals_whole_file_hash():
    """The streamed content_id must equal the OLD whole-array hash, or every
    deployed cache entry silently invalidates on deploy."""
    y = (0.2 * np.sin(2 * np.pi * 220 * np.linspace(0, 5, 5 * SR, endpoint=False))).astype("float32")
    p = _write(y)
    # reproduce the pre-#7 computation: sha256(whole_pcm.tobytes() + str(sr))
    data, sr = sf.read(p, dtype="float32", always_2d=False)
    h = hashlib.sha256()
    h.update(np.ascontiguousarray(data).tobytes())
    h.update(str(sr).encode())
    expected = "cid_" + h.hexdigest()[:32]
    assert cache.content_id(p) == expected


def test_waveform_reduction_is_exact_vs_single_pass():
    """Streamed per-bin min/max must equal a single-pass numpy computation."""
    y = (0.4 * np.sin(2 * np.pi * 110 * np.linspace(0, 8, 8 * SR, endpoint=False))).astype("float32")
    p = _write(y)
    bins = 64
    r = signal.waveform(p, bins=bins)
    edges = np.linspace(0, len(y), bins + 1).astype(int)
    for i in range(bins):
        seg = y[edges[i]:edges[i + 1]]
        assert abs(r["peaks"][i][0] - round(float(seg.min()), 4)) < 1e-3
        assert abs(r["peaks"][i][1] - round(float(seg.max()), 4)) < 1e-3


def test_streaming_windows_cover_all_samples_once():
    """The window iterator must yield every sample exactly once, contiguously."""
    y = np.arange(int(2.5 * SR), dtype="float32")
    p = _write(y)
    seen = 0
    last_end = 0
    for block, sr, start in streaming.windows(p, win_s=1.0, mono=True):
        assert start == last_end                    # contiguous, no gap/overlap
        seen += len(block)
        last_end = start + len(block)
    assert seen == len(y)
