//! Include-based coverage for microphone source-selection helpers.
//!
//! Scope: include `client/src/input/microphone.rs` and exercise Pulse source
//! parsing + fallback behavior without requiring a live audio stack.
//! Targets: `client/src/input/microphone.rs`.
//! Why: source selection regressions should be caught with deterministic tests.

#[allow(warnings)]
mod live_capture_clock {
    include!("support/live_capture_clock_shim.rs");
}

#[allow(warnings)]
mod microphone_include_contract {
    include!(env!("LESAVKA_CLIENT_MICROPHONE_SRC"));

    use serial_test::serial;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use std::path::Path;
    use temp_env::with_var;
    use tempfile::tempdir;

    fn write_executable(dir: &Path, name: &str, body: &str) {
        let path = dir.join(name);
        fs::write(&path, body).expect("write script");
        let mut perms = fs::metadata(&path).expect("metadata").permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms).expect("chmod");
    }

    fn with_fake_command(name: &str, script_body: &str, f: impl FnOnce()) {
        let dir = tempdir().expect("tempdir");
        write_executable(dir.path(), name, script_body);
        let prior = std::env::var("PATH").unwrap_or_default();
        let merged = if prior.is_empty() {
            dir.path().display().to_string()
        } else {
            format!("{}:{prior}", dir.path().display())
        };
        with_var("PATH", Some(merged), f);
    }

    fn with_fake_pactl(script_body: &str, f: impl FnOnce()) {
        with_fake_command("pactl", script_body, f);
    }

    fn with_fake_pw_dump(script_body: &str, f: impl FnOnce()) {
        with_fake_command("pw-dump", script_body, f);
    }

    #[test]
    #[serial]
    fn pulse_source_by_substr_matches_expected_device_name() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then
  echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING"
  echo "1 alsa_input.usb-Mic_1234-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE"
  exit 0
fi
exit 0
"#;
        with_fake_pactl(script, || {
            let src =
                MicrophoneCapture::pulse_source_by_substr("Mic_1234").expect("matching source");
            assert_eq!(src, "alsa_input.usb-Mic_1234-00.analog-stereo");
        });
    }

    #[test]
    #[serial]
    fn pulse_source_desc_formats_selected_non_monitor_source() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then
  echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING"
  echo "1 alsa_input.usb-DeskMic_5678-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE"
  exit 0
fi
exit 0
"#;
        with_fake_pactl(script, || {
            let source =
                MicrophoneCapture::pulse_source_by_substr("DeskMic_5678").expect("matching source");
            let desc = MicrophoneCapture::pulse_source_desc(Some(&source));
            assert!(
                desc.contains("pulsesrc device=alsa_input.usb-DeskMic_5678-00.analog-stereo"),
                "expected escaped non-monitor source argument: {desc}"
            );
        });
    }

    #[test]
    #[serial]
    fn pulse_source_by_substr_returns_none_when_pactl_is_unavailable() {
        with_var("PATH", Some("/definitely/missing/path"), || {
            assert!(MicrophoneCapture::pulse_source_by_substr("anything").is_none());
            assert_eq!(
                MicrophoneCapture::pulse_source_desc(None),
                "pulsesrc do-timestamp=true"
            );
        });
    }

    #[test]
    fn pipewire_source_desc_formats_selected_and_default_sources() {
        let selected = MicrophoneCapture::pipewire_source_desc(Some("alsa input/Desk Mic"));
        assert!(
            selected.contains("pipewiresrc target-object='alsa input/Desk Mic'"),
            "expected shell-escaped PipeWire target: {selected}"
        );
        assert_eq!(
            MicrophoneCapture::pipewire_source_desc(Some("   ")),
            "pipewiresrc do-timestamp=true"
        );
        assert_eq!(
            MicrophoneCapture::pipewire_source_desc(None),
            "pipewiresrc do-timestamp=true"
        );
    }

    #[test]
    #[serial]
    fn pipewire_source_by_substr_prefers_audio_sources_and_skips_monitors() {
        let script = r#"#!/usr/bin/env sh
cat <<'JSON'
[
  {"info":{"props":{"media.class":"Audio/Sink","node.name":"alsa_output.usb-headphones"}}},
  {"info":{"props":{"media.class":"Audio/Source","node.name":"alsa_input.usb-DeskMic.monitor"}}},
  {"info":{"props":{"media.class":"Audio/Source","node.name":"alsa_input.usb-DeskMic"}}},
  {"info":{"props":{"media.class":"Audio/Source","node.nick":"Fallback Nick Mic"}}}
]
JSON
"#;
        with_fake_pw_dump(script, || {
            assert_eq!(
                MicrophoneCapture::pipewire_source_by_substr("DeskMic").as_deref(),
                Some("alsa_input.usb-DeskMic")
            );
            assert_eq!(
                MicrophoneCapture::pipewire_source_by_substr("Fallback Nick").as_deref(),
                Some("Fallback Nick Mic")
            );
            assert!(MicrophoneCapture::pipewire_source_by_substr("missing").is_none());
        });
    }

    #[test]
    fn default_source_desc_selects_a_valid_gstreamer_source_description() {
        gst::init().ok();
        let desc = MicrophoneCapture::default_source_desc();
        assert!(
            desc == "pipewiresrc do-timestamp=true" || desc == "pulsesrc do-timestamp=true",
            "default source should stay a simple PipeWire/Pulse source: {desc}"
        );
    }

    #[test]
    #[serial]
    fn mic_gain_env_defaults_and_clamps_for_uplink_gain() {
        with_var("LESAVKA_MIC_GAIN", None::<&str>, || {
            assert_eq!(mic_gain_from_env(), 1.0);
        });
        with_var("LESAVKA_MIC_GAIN", Some("2.75"), || {
            assert_eq!(mic_gain_from_env(), 2.75);
        });
        with_var("LESAVKA_MIC_GAIN", Some("99"), || {
            assert_eq!(mic_gain_from_env(), 4.0);
        });
        with_var("LESAVKA_MIC_GAIN", Some("-1"), || {
            assert_eq!(mic_gain_from_env(), 0.0);
        });
        with_var("LESAVKA_MIC_GAIN", Some("bad"), || {
            assert_eq!(mic_gain_from_env(), 1.0);
        });
    }

    #[test]
    fn microphone_pipeline_desc_adds_level_tap_only_when_requested() {
        assert!(parser_for_encoder("opusenc").contains("audio/x-opus"));
        assert!(parser_for_encoder("avenc_aac").contains("audio/mpeg"));

        let with_tap = microphone_pipeline_desc(
            "audiotestsrc is-live=true",
            "opusenc",
            parser_for_encoder("opusenc"),
            2.5,
            true,
        );
        assert!(
            with_tap
                .contains("audiotestsrc is-live=true ! audioconvert ! audioresample ! audio/x-raw")
        );
        assert!(!with_tap.contains("audiotestsrc is-live=true ! audio/x-raw"));
        assert!(with_tap.contains("tee name=t"));
        assert!(with_tap.contains("appsink name=level_sink"));
        assert!(with_tap.contains("volume name=mic_input_gain volume=2.500"));

        let without_tap = microphone_pipeline_desc(
            "audiotestsrc is-live=true",
            "avenc_aac",
            parser_for_encoder("avenc_aac"),
            1.0,
            false,
        );
        assert!(!without_tap.contains("level_sink"));
        assert!(without_tap.contains("queue max-size-buffers=100 leaky=downstream"));
    }

    #[test]
    fn mic_gain_control_reads_first_token_and_clamps() {
        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("mic-gain.control");
        fs::write(&path, "3.250 nonce\n").expect("write gain");
        assert_eq!(read_mic_gain_control(&path), Some(3.25));

        fs::write(&path, "20.0 nonce\n").expect("write clamped gain");
        assert_eq!(read_mic_gain_control(&path), Some(4.0));

        fs::write(&path, "bad nonce\n").expect("write invalid gain");
        assert_eq!(read_mic_gain_control(&path), None);
    }

    #[test]
    #[serial]
    fn mic_level_tap_env_and_payload_helpers_are_stable() {
        with_var("LESAVKA_UPLINK_MIC_LEVEL", None::<&str>, || {
            assert!(mic_level_tap_path().is_none());
        });

        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("uplink-mic-level.value");
        with_var(
            "LESAVKA_UPLINK_MIC_LEVEL",
            Some(path.to_string_lossy().to_string()),
            || {
                assert_eq!(mic_level_tap_path().as_deref(), Some(path.as_path()));
            },
        );

        assert_eq!(pcm_peak_fraction(&0_i16.to_le_bytes()), 0.0);
        assert!(pcm_peak_fraction(&i16::MAX.to_le_bytes()) > 0.99);

        write_mic_level_tap(&path, 0.375).expect("write level tap");
        assert_eq!(
            fs::read_to_string(&path).expect("read level tap").trim(),
            "0.375000"
        );
    }

    #[test]
    #[serial]
    fn mic_gain_control_returns_without_env() {
        gst::init().ok();
        let volume = gst::ElementFactory::make("volume")
            .build()
            .expect("volume element");
        volume.set_property("volume", 1.75_f64);

        with_var("LESAVKA_MIC_GAIN_CONTROL", None::<&str>, || {
            maybe_spawn_mic_gain_control(volume.clone());
        });

        assert_eq!(volume.property::<f64>("volume"), 1.75);
    }

    #[test]
    #[serial]
    fn mic_gain_control_updates_volume_element_live() {
        gst::init().ok();
        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("mic-gain.control");
        fs::write(&path, "2.500 nonce\n").expect("write gain");
        let volume = gst::ElementFactory::make("volume")
            .build()
            .expect("volume element");

        with_var(
            "LESAVKA_MIC_GAIN_CONTROL",
            Some(path.to_string_lossy().to_string()),
            || {
                maybe_spawn_mic_gain_control(volume.clone());
                for _ in 0..20 {
                    if (volume.property::<f64>("volume") - 2.5).abs() < 0.001 {
                        return;
                    }
                    std::thread::sleep(std::time::Duration::from_millis(25));
                }
            },
        );

        assert!(
            (volume.property::<f64>("volume") - 2.5).abs() < 0.001,
            "live mic gain control should update the GStreamer volume"
        );
    }

    #[test]
    fn pull_returns_none_for_empty_appsink() {
        gst::init().ok();
        let sink: gst_app::AppSink = gst::ElementFactory::make("appsink")
            .build()
            .expect("appsink")
            .downcast::<gst_app::AppSink>()
            .expect("appsink cast");
        let running = std::sync::Arc::new(AtomicBool::new(true));
        let cap = MicrophoneCapture {
            pipeline: gst::Pipeline::new(),
            sink,
            level_tap_running: Some(std::sync::Arc::clone(&running)),
            pts_rebaser: crate::live_capture_clock::SourcePtsRebaser::default(),
        };
        assert!(
            cap.pull().is_none(),
            "empty appsink should produce no packet"
        );
        drop(cap);
        assert!(!running.load(AtomicOrdering::Acquire));
    }

    #[test]
    fn spawned_mic_level_tap_publishes_peak_from_appsink() {
        gst::init().ok();
        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("mic-level.value");
        let pipeline: gst::Pipeline = gst::parse::launch(
            "appsrc name=src is-live=true format=time caps=audio/x-raw,format=S16LE,channels=2,rate=48000 ! \
             appsink name=sink emit-signals=false sync=false max-buffers=4 drop=true",
        )
        .expect("pipeline")
        .downcast()
        .expect("pipeline cast");
        let src: gst_app::AppSrc = pipeline
            .by_name("src")
            .expect("appsrc")
            .downcast()
            .expect("appsrc cast");
        let sink: gst_app::AppSink = pipeline
            .by_name("sink")
            .expect("appsink")
            .downcast()
            .expect("appsink cast");
        pipeline.set_state(gst::State::Playing).expect("playing");

        let running = spawn_mic_level_tap(sink, path.clone());
        src.push_buffer(gst::Buffer::from_slice(i16::MAX.to_le_bytes().repeat(4)))
            .expect("push buffer");

        for _ in 0..20 {
            if let Ok(raw) = fs::read_to_string(&path) {
                let level = raw.trim().parse::<f64>().expect("level");
                assert!(level > 0.99);
                running.store(false, AtomicOrdering::Release);
                let _ = pipeline.set_state(gst::State::Null);
                return;
            }
            std::thread::sleep(std::time::Duration::from_millis(25));
        }

        running.store(false, AtomicOrdering::Release);
        let _ = pipeline.set_state(gst::State::Null);
        panic!("microphone level tap did not publish a value");
    }

    #[test]
    #[cfg(coverage)]
    #[serial]
    fn microphone_capture_with_level_tap_uses_the_same_uplink_pipeline() {
        gst::init().ok();
        let dir = tempdir().expect("tempdir");
        let level_path = dir.path().join("uplink-mic-level.value");

        with_var("LESAVKA_MIC_SOURCE", None::<&str>, || {
            with_var(
                "LESAVKA_MIC_TEST_SOURCE_DESC",
                Some("audiotestsrc is-live=true wave=sine freq=440".to_string()),
                || {
                    with_var(
                        "LESAVKA_UPLINK_MIC_LEVEL",
                        Some(level_path.to_string_lossy().to_string()),
                        || {
                            let cap = MicrophoneCapture::new().expect("synthetic mic capture");
                            assert!(cap.level_tap_running.is_some());
                        },
                    );
                },
            );
        });
    }

    #[test]
    fn pull_returns_packet_when_appsink_has_buffered_sample_with_shared_capture_clock_pts() {
        gst::init().ok();
        let pipeline = gst::Pipeline::new();
        let src = gst::ElementFactory::make("appsrc")
            .build()
            .expect("appsrc")
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc cast");
        let sink = gst::ElementFactory::make("appsink")
            .property("emit-signals", false)
            .property("sync", false)
            .build()
            .expect("appsink")
            .downcast::<gst_app::AppSink>()
            .expect("appsink cast");
        pipeline
            .add_many([
                src.upcast_ref::<gst::Element>(),
                sink.upcast_ref::<gst::Element>(),
            ])
            .expect("add appsrc/appsink");
        src.link(&sink).expect("link appsrc->appsink");
        pipeline.set_state(gst::State::Playing).ok();

        let mut first = gst::Buffer::from_slice(vec![1_u8, 2, 3, 4]);
        first
            .get_mut()
            .expect("buffer mut")
            .set_pts(Some(gst::ClockTime::from_useconds(321)));
        src.push_buffer(first).expect("push first sample");

        let mut second = gst::Buffer::from_slice(vec![5_u8, 6, 7, 8]);
        second
            .get_mut()
            .expect("buffer mut")
            .set_pts(Some(gst::ClockTime::from_useconds(999_999)));
        src.push_buffer(second).expect("push second sample");

        let cap = MicrophoneCapture {
            pipeline,
            sink,
            level_tap_running: None,
            pts_rebaser: crate::live_capture_clock::SourcePtsRebaser::default(),
        };
        let first_pkt = cap.pull().expect("first audio packet");
        let second_pkt = cap.pull().expect("second audio packet");
        assert_eq!(first_pkt.id, 0);
        assert_eq!(first_pkt.data, vec![1, 2, 3, 4]);
        assert_eq!(second_pkt.data, vec![5, 6, 7, 8]);
        assert!(second_pkt.pts >= first_pkt.pts);
        assert_ne!(first_pkt.pts, 321);
        assert_ne!(second_pkt.pts, 999_999);
    }

    #[test]
    #[serial]
    fn new_uses_requested_source_fragment_when_available() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then
  echo "1 alsa_input.usb-LavMic_abc-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz RUNNING"
  exit 0
fi
exit 0
"#;
        with_fake_pactl(script, || {
            with_var("LESAVKA_MIC_SOURCE", Some("LavMic_abc"), || {
                let result = MicrophoneCapture::new();
                if let Err(err) = result {
                    assert!(!err.to_string().trim().is_empty());
                }
            });
        });
    }

    #[test]
    #[serial]
    fn resolve_source_desc_prefers_pipewire_named_source_when_available() {
        if !MicrophoneCapture::pipewire_source_available() {
            return;
        }

        let script = r#"#!/usr/bin/env sh
cat <<'JSON'
[
  {"info":{"props":{"media.class":"Audio/Source","node.name":"alsa_input.usb-UpstreamMic"}}}
]
JSON
"#;
        with_fake_pw_dump(script, || {
            let desc =
                MicrophoneCapture::resolve_source_desc("UpstreamMic").expect("pipewire source");
            assert!(desc.contains("pipewiresrc target-object=alsa_input.usb-UpstreamMic"));
        });
    }

    #[test]
    #[serial]
    fn new_falls_back_to_default_source_when_requested_fragment_is_missing() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then
  echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING"
  echo "1 alsa_input.usb-DeskMic_777-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE"
  exit 0
fi
exit 0
"#;
        with_fake_pactl(script, || {
            with_var("LESAVKA_MIC_SOURCE", Some("missing-fragment"), || {
                let result = MicrophoneCapture::new();
                if let Err(err) = result {
                    assert!(!err.to_string().trim().is_empty());
                }
            });
        });
    }
}
