// Integration coverage for server audio capture/sink plumbing.
//
// Scope: compile the split server audio module and exercise public audio
// constructors/helpers across deterministic error and smoke paths.
// Targets: `server/src/audio.rs` plus `server/src/audio/*`.
// Why: audio pipeline setup is branchy and should stay stable without requiring
// physical ALSA/UAC hardware in CI.

pub use lesavka_server::camera;

#[path = "../../../../server/src/media_timing.rs"]
#[allow(warnings)]
mod media_timing;

#[path = "../../../../server/src/audio.rs"]
#[allow(warnings)]
mod server_audio_contract;

mod tests {
    use super::server_audio_contract::{ClipTap, Voice, ear, start_pipeline_or_reset};
    #[cfg(coverage)]
    use futures_util::StreamExt;
    use gstreamer as gst;
    use gstreamer::prelude::*;
    use lesavka_common::lesavka::AudioPacket;
    use serial_test::serial;

    const AUDIO_SRC: &str = concat!(
        include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/server/src/audio.rs")),
        include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/server/src/audio/ear_capture.rs"
        )),
        include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/server/src/audio/ear_capture/source_watchdog.rs"
        )),
        include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/server/src/audio/voice_input.rs"
        )),
    );

    fn source_index(needle: &str) -> usize {
        AUDIO_SRC
            .find(needle)
            .unwrap_or_else(|| panic!("missing source marker: {needle}"))
    }

    #[test]
    fn failed_pipeline_start_contract_resets_and_does_not_spawn_bus_first() {
        assert!(AUDIO_SRC.contains("let _ = pipeline.set_state(gst::State::Null);"));
        assert!(AUDIO_SRC.contains("impl Drop for Voice"));
        assert!(
            source_index("start_pipeline_or_reset(&pipeline, \"starting audio pipeline\")?")
                < source_index("spawn_pipeline_bus_logger(bus, \"audio\"")
        );
        assert!(
            source_index("start_pipeline_or_reset(&pipeline, \"starting voice pipeline\")?")
                < source_index("spawn_pipeline_bus_logger(bus, \"voice\"")
        );
    }

    #[test]
    fn speaker_capture_watchdog_does_not_hard_fail_idle_audio_by_default() {
        assert!(AUDIO_SRC.contains("LESAVKA_AUDIO_FAIL_ON_IDLE"));
        assert!(AUDIO_SRC.contains("let fail_on_idle = env_bool(\"LESAVKA_AUDIO_FAIL_ON_IDLE\")"));
        assert!(AUDIO_SRC.contains("env_u64(\"LESAVKA_AUDIO_MIN_PACKETS_PER_SEC\", 0)"));
        assert!(AUDIO_SRC.contains("remote speaker capture cadence is too low"));
    }

    #[test]
    #[serial]
    fn start_pipeline_or_reset_starts_empty_pipeline() {
        let _ = gst::init();
        let pipeline = gst::Pipeline::new();
        start_pipeline_or_reset(&pipeline, "starting contract pipeline")
            .expect("empty pipeline should enter playing");
        assert_eq!(pipeline.current_state(), gst::State::Playing);
        let _ = pipeline.set_state(gst::State::Null);
    }

    #[cfg(coverage)]
    #[test]
    #[serial]
    fn start_pipeline_or_reset_forced_failure_resets_pipeline() {
        let _ = gst::init();
        let pipeline = gst::Pipeline::new();
        temp_env::with_var("LESAVKA_TEST_FORCE_PIPELINE_START_ERROR", Some("1"), || {
            let err = start_pipeline_or_reset(&pipeline, "starting forced contract pipeline")
                .expect_err("forced coverage error");
            assert!(err.to_string().contains("forced test failure"));
        });
        assert_eq!(pipeline.current_state(), gst::State::Null);
    }

    #[test]
    #[serial]
    fn ear_rejects_malformed_pipeline_device_string() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let result = rt.block_on(ear("hw:UAC2Gadget,0\" ! broken-pipe", 0));
        assert!(result.is_err(), "malformed device string should fail parse");
    }

    #[test]
    #[serial]
    fn ear_missing_device_path_is_stable() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let result = rt.block_on(ear("hw:DefinitelyMissingDevice,0", 0));
        if let Err(err) = result {
            assert!(!err.to_string().trim().is_empty());
        }
    }

    #[test]
    #[serial]
    fn ear_existing_non_audio_node_reaches_runtime_paths() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let result = rt.block_on(async {
            tokio::time::timeout(std::time::Duration::from_millis(250), ear("/dev/null", 0)).await
        });
        if let Ok(Ok(stream)) = result {
            drop(stream)
        }
    }

    #[test]
    fn clip_tap_feed_flush_and_drop_are_stable() {
        let mut tap = ClipTap::new("audio-contract", std::time::Duration::from_millis(1));
        tap.feed(&[1, 2, 3, 4, 5]);
        tap.feed(&vec![9u8; 300_000]);
        tap.flush();
        tap.flush(); // empty flush should be a no-op
        drop(tap);
    }

    #[test]
    #[serial]
    fn voice_constructor_and_push_finish_are_stable() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let result = rt.block_on(Voice::new("hw:DefinitelyMissingDevice,0"));
        match result {
            Ok(mut voice) => {
                voice.push(&AudioPacket {
                    id: 0,
                    pts: 77,
                    data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC],
                    ..AudioPacket::default()
                });
                voice.finish();
            }
            Err(err) => {
                assert!(!err.to_string().trim().is_empty());
            }
        }
    }

    #[cfg(coverage)]
    #[test]
    #[serial]
    fn audio_stream_poll_next_is_stable_when_channel_is_closed() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let polled = rt.block_on(async {
            let mut stream = ear("/dev/null", 0).await.expect("coverage ear stream");
            stream.next().await
        });
        assert!(polled.is_none(), "closed stream should yield None");
    }
}
