//! slice-3 integration test: brain-process-level round-trip through the
//! MockRealtimeBrain + the translator + `run_openai_pump`. Drives the brain
//! the way the core's TapClient would (synthetic tap frames in, observes tap
//! frames out) — proves the dev-loop interop contract the PM directive's
//! Task 4 calls out ("actually start + interop on a basic audio round-trip")
//! without requiring a real WebRTC peer (browser-driven, same constraint as
//! slice-1/2).
//!
//! What this exercises end-to-end:
//! 1. MockRealtimeBrain accepts a WS connection + asserts session.update
//!    has turn_detection: null (S4 — spec §4.3).
//! 2. `run_openai_pump` sends session.update + bridges tap frames ↔ OpenAI
//!    events via the translator.
//! 3. A tap `audio_in` frames the test pushes through `tap_via` → translator
//!    formats as `input_audio_buffer.append` → mock generates a canned
//!    `response.audio.delta` → translator formats as tap `audio_out` →
//!    arrives on `pump_tap_tx` (the pump's outbound mpsc).

use rutster_brain_realtime::{MockRealtimeBrain, openai_client::run_openai_pump};
use rutster_tap::{PcmFrame, decode_envelope, encode_audio_in, encode_hello};
use serde_json::Value;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;

#[tokio::test]
async fn brain_mock_round_trip_translates_audio_in_to_audio_out() {
    let mock = MockRealtimeBrain::start().await.unwrap();
    let url = mock.url();
    let req = url.as_str().into_client_request().unwrap();
    let (openai_ws, _resp) = tokio_tungstenite::connect_async(req).await.unwrap();

    // Bridge channels matching `run_openai_pump`'s signature (it owns neither
    // side of the tap WS — the binary's main.rs does the WS forwarding; here
    // we drive the pump directly with mpsc as the test's tap peer).
    let (tap_via, pump_tap_rx) = mpsc::channel::<String>(64);
    let (pump_tap_tx, mut tap_out_rx) = mpsc::channel::<String>(64);

    // The pump sends session.update + then runs the select! loop. Spawn it.
    let pump = tokio::spawn(async move {
        run_openai_pump(openai_ws, pump_tap_rx, pump_tap_tx, "alloy".to_string()).await
    });

    // Push a hello (the pump ignores hello per its `_ => warn!(...)` arm,
    // but real tap traffic always starts with hello).
    let hello = encode_hello("test-session", 0, 0).unwrap();
    tap_via.send(hello).await.unwrap();

    // Push one audio_in frame. The pump translates to
    // input_audio_buffer.append; the mock replies with a canned
    // response.audio.delta; the pump translates back to a tap audio_out
    // frame on pump_tap_tx.
    let frame = PcmFrame::zeroed();
    let audio_in = encode_audio_in(&frame, 1, 100).unwrap();
    tap_via.send(audio_in).await.unwrap();

    // Bounded-wait the audio_out frame coming back. Generous bound: the
    // pump's tokio select! cycle + the mock's WS round-trip on loopback
    // are sub-millisecond; 500 ms is fail-fast for any real regression.
    let out_str = tokio::time::timeout(Duration::from_millis(500), tap_out_rx.recv())
        .await
        .expect("audio_out within 500ms")
        .expect("channel not closed");

    let decoded = decode_envelope(&out_str).unwrap();
    match decoded.payload {
        rutster_tap::DecodedPayload::AudioOut(p) => {
            // The mock's canned PCM is 480 zeroed samples; the brain's
            // translator round-trips it through PcmFrame (decode +
            // re-encode), so the wire shape is exactly slice-2's audio_out.
            assert_eq!(p.samples, 480);
            let recovered = rutster_tap::decode_pcm(&p.pcm, p.samples).unwrap();
            assert_eq!(recovered.samples.len(), 480);
            // Canned zeros — every sample is 0.
            assert_eq!(recovered.samples[0], 0);
            assert_eq!(recovered.samples[479], 0);
            // seq + ts carried through from the translator's per-event
            // counter (initial value 0 on the first outbound frame).
            assert_eq!(decoded.seq, 0);
        }
        other => panic!("expected AudioOut, got {other:?}"),
    }

    // Drop the input channel to signal the pump to exit cleanly.
    drop(tap_via);
    let _ = pump.await;
    let _ = Value::Null; // suppress unused-import if Value unused here
}
