//! Smoke test for the macOS Apple-Speech STT provider.
//!
//! Round-trip:
//! 1. Synth a known phrase via `say(1)` to a WAV file
//! 2. Decode the WAV back to f32 PCM
//! 3. Hand the samples to `AppleSpeechSttProvider::transcribe`
//! 4. Compare the recognized text to the expected phrase
//!
//! ## CLI vs bundled-app caveat — validated empirically on M5 / macOS 26.4.1
//!
//! `SFSpeechRecognizer` only delivers results to **Developer ID-signed
//! bundles**. Ad-hoc signed or unsigned binaries (the default for
//! `cargo run`) are blocked by Apple Mobile File Integrity (AMFI):
//!
//! ```text
//! amfid: /path/to/binary not valid:
//!   Error Domain=AppleMobileFileIntegrityError Code=-423
//!   "The file is adhoc signed or signed by an unknown certificate chain"
//! ```
//!
//! When AMFI rejects the binary, the recognition XPC service still
//! spawns and processes the request (we observed it running for the
//! full 60 s timeout window), but the result-handler callback is
//! silently dropped — the provider's safety timeout fires.
//!
//! Things we tried and verified do NOT unblock the CLI path:
//! - `NSSpeechRecognitionUsageDescription` + bundle ID via embedded
//!   `__TEXT,__info_plist` section — fixes the SFUtilities fault but
//!   not the AMFI gate
//! - Wrapping the binary in a `.app` bundle structure
//! - `codesign --sign - --force --deep` (ad-hoc signing)
//! - Explicit `requestAuthorization` call that does fire its own block
//! - `allow_server()` to bypass on-device recognition path
//! - `requiresOnDeviceRecognition` toggled both ways
//!
//! Inside a Developer ID-signed host app (the actual product shape),
//! the same code paths work. This smoke exists to validate the
//! provider's hardening (auth probe, timeout safety, task cancel) and
//! to capture what the dev-loop reproduces on a CLI binary, NOT to
//! test the happy path. The host-app integration tests cover the
//! latter.
//!
//! Run from the repo root:
//!     cd car-rs && cargo run --release -p car-voice --example apple_speech_stt_smoke

#[cfg(target_os = "macos")]
#[tokio::main]
async fn main() {
    use car_voice::{AppleSpeechSttProvider, SttProvider};

    let phrase = "the quick brown fox jumps over the lazy dog";
    println!("=== AppleSpeech STT smoke test ===");
    println!("expected text: {}", phrase);
    println!();

    // Probe NSBundle.main to confirm the embedded Info.plist is visible.
    {
        use objc2_foundation::NSBundle;
        let main = unsafe { NSBundle::mainBundle() };
        let bid = unsafe { main.bundleIdentifier() }.map(|s| s.to_string());
        let path = unsafe { main.bundlePath() }.to_string();
        let info = unsafe { main.infoDictionary() };
        println!("   NSBundle.mainBundle().bundleIdentifier = {:?}", bid);
        println!("   NSBundle.mainBundle().bundlePath = {}", path);
        println!("   infoDictionary present = {}", info.is_some());
    }
    println!();

    // Probe auth + recognizer state up front so we can tell auth
    // failures from runtime failures.
    {
        use block2::RcBlock;
        use objc2::rc::Retained;
        use objc2_speech::{SFSpeechRecognizer, SFSpeechRecognizerAuthorizationStatus};

        let status_before = unsafe { SFSpeechRecognizer::authorizationStatus() };
        println!(
            "   authorizationStatus (before requestAuthorization) = {:?}",
            status_before
        );

        // Explicitly call requestAuthorization. Some Apple frameworks
        // require this to register the calling process with the
        // recognition daemon even when the system-wide status already
        // reads Authorized.
        let (tx, rx) = std::sync::mpsc::sync_channel::<SFSpeechRecognizerAuthorizationStatus>(1);
        let handler: RcBlock<dyn Fn(SFSpeechRecognizerAuthorizationStatus)> =
            RcBlock::new(move |s: SFSpeechRecognizerAuthorizationStatus| {
                let _ = tx.try_send(s);
            });
        unsafe { SFSpeechRecognizer::requestAuthorization(&*handler) };
        match rx.recv_timeout(std::time::Duration::from_secs(5)) {
            Ok(s) => println!("   requestAuthorization callback → {:?}", s),
            Err(_) => {
                println!("   requestAuthorization callback timed out (no prompt allowed in CLI)")
            }
        }

        let status = unsafe { SFSpeechRecognizer::authorizationStatus() };
        println!("   authorizationStatus (after) = {:?}", status);
        let recognizer: Retained<SFSpeechRecognizer> = unsafe { SFSpeechRecognizer::new() };
        let avail = unsafe { recognizer.isAvailable() };
        println!("   recognizer.isAvailable = {}", avail);
        let supports_on_device = unsafe { recognizer.supportsOnDeviceRecognition() };
        println!(
            "   recognizer.supportsOnDeviceRecognition = {}",
            supports_on_device
        );
        if status != SFSpeechRecognizerAuthorizationStatus::Authorized {
            println!();
            println!("   Authorization not granted. Skipping transcription.");
            return;
        }
        if !avail {
            println!();
            println!("   Recognizer reports unavailable; nothing further to test.");
            return;
        }
    }
    println!();

    // Step 1: synthesize phrase to WAV via say(1).
    let tmp = match tempfile::Builder::new()
        .prefix("car-stt-smoke-")
        .suffix(".wav")
        .tempfile()
    {
        Ok(t) => t,
        Err(e) => {
            eprintln!("tempfile: {e}");
            std::process::exit(1);
        }
    };
    let wav_path = tmp.path().to_owned();
    let say_status = std::process::Command::new("/usr/bin/say")
        .arg("-o")
        .arg(&wav_path)
        .arg("--file-format=WAVE")
        .arg("--data-format=LEI16@16000")
        .arg("-v")
        .arg("Samantha")
        .arg(phrase)
        .status();
    match say_status {
        Ok(s) if s.success() => {
            println!("1. say(1) synthesized -> {}", wav_path.display());
        }
        Ok(s) => {
            eprintln!("say(1) exited {}", s);
            std::process::exit(1);
        }
        Err(e) => {
            eprintln!("say(1) spawn: {e}");
            std::process::exit(1);
        }
    }

    // Step 2: decode WAV -> f32 PCM at 16k.
    let mut reader = match hound::WavReader::open(&wav_path) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("wav open: {e}");
            std::process::exit(1);
        }
    };
    let spec = reader.spec();
    println!(
        "   WAV spec: channels={} sample_rate={} bits={} format={:?}",
        spec.channels, spec.sample_rate, spec.bits_per_sample, spec.sample_format
    );
    let samples: Vec<f32> = match spec.sample_format {
        hound::SampleFormat::Int => reader
            .samples::<i16>()
            .map(|s| s.map(|v| v as f32 / i16::MAX as f32).unwrap_or(0.0))
            .collect(),
        hound::SampleFormat::Float => reader.samples::<f32>().map(|s| s.unwrap_or(0.0)).collect(),
    };
    println!(
        "   decoded {} samples ({} ms)",
        samples.len(),
        1000 * samples.len() as u32 / spec.sample_rate
    );

    // Step 3: feed to AppleSpeechSttProvider.
    println!();
    println!("2. transcribe via SFSpeechRecognizer + SFSpeechURLRecognitionRequest");
    // Allow server-side recognition — the on-device-only path may
    // require a model that isn't provisioned for this locale even
    // when the recognizer reports supportsOnDeviceRecognition=true.
    let provider = AppleSpeechSttProvider::new().allow_server();
    let started = std::time::Instant::now();
    match provider.transcribe(&samples, spec.sample_rate).await {
        Ok(text) => {
            let ms = started.elapsed().as_millis();
            println!("   recognized ({} ms): {:?}", ms, text);
            let normalized = text.to_lowercase().replace(['.', ',', '!', '?'], "");
            let expected_normalized = phrase.to_lowercase();
            if normalized.contains(&expected_normalized)
                || expected_normalized.contains(&normalized)
            {
                println!("   ✓ matches expected (case/punctuation insensitive)");
            } else {
                println!("   △ differs from expected — Apple's recognition is good but not");
                println!("     identical to input; for round-trip this is acceptable as long");
                println!("     as the result is non-empty and English-shaped");
            }
        }
        Err(e) => {
            let msg = e.to_string();
            if msg.contains("timed out") {
                println!("   recognition timed out — expected for an ad-hoc / unsigned binary.");
                println!("   Apple Mobile File Integrity (amfid) rejects binaries that aren't");
                println!("   Developer ID-signed; the recognition XPC service runs but the");
                println!("   result-handler callback is silently dropped. Our 60 s safety");
                println!("   timeout caught the hang and the task was cancelled.");
                println!();
                println!("   To exercise the happy path: code-sign with a real Developer ID");
                println!("   certificate (or run inside a properly-signed host app like the");
                println!("   Parslee menubar build).");
                println!();
                println!("   Skipping streaming variant for the same reason.");
                return;
            }
            eprintln!("   unexpected error: {}", e);
            std::process::exit(2);
        }
    }

    // Step 4: streaming variant.
    println!();
    println!("3. transcribe_streaming with on_partial callback");
    let started = std::time::Instant::now();
    let mut partials_seen = 0_usize;
    let on_partial = |p: car_voice::apple_speech_stt::AppleSpeechPartial| {
        partials_seen += 1;
        println!("   partial #{}: {:?}", partials_seen, p.text);
    };
    match provider
        .transcribe_streaming(&samples, spec.sample_rate, on_partial)
        .await
    {
        Ok(text) => {
            let ms = started.elapsed().as_millis();
            println!(
                "   final ({} ms, {} partials): {:?}",
                ms, partials_seen, text
            );
        }
        Err(e) => {
            eprintln!("   streaming failed: {}", e);
            std::process::exit(2);
        }
    }

    drop(tmp);
    println!("\nAppleSpeech STT paths green.");
}

#[cfg(not(target_os = "macos"))]
fn main() {
    eprintln!("Apple-Speech providers are macOS only.");
    std::process::exit(2);
}
