// Include-based coverage for client audio output sink selection helpers.
//
// Scope: include `client/src/output/audio.rs` and exercise sink discovery
// branches with controlled `pactl` fixtures.
// Targets: `client/src/output/audio.rs`.
// Why: keep sink-resolution behavior deterministic without requiring live
// desktop audio devices in CI.

#[allow(warnings)]
mod audio_include_contract {
    include!(env!("LESAVKA_CLIENT_OUTPUT_AUDIO_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_pactl(script_body: &str, f: impl FnOnce()) {
        let dir = tempdir().expect("tempdir");
        write_executable(dir.path(), "pactl", 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);
    }

    #[test]
    #[serial]
    fn pick_sink_element_prefers_operator_override() {
        with_var("LESAVKA_AUDIO_SINK", Some("fakesink sync=false"), || {
            let sink = pick_sink_element(None, true).expect("override sink");
            assert_eq!(sink, "fakesink sync=false");
        });
    }

    #[test]
    #[serial]
    fn pick_sink_element_wraps_bare_device_override_for_pulsesink() {
        with_var(
            "LESAVKA_AUDIO_SINK",
            Some("alsa_output.pci-0000_00_1f.3.analog-stereo"),
            || {
                let sink = pick_sink_element(None, true).expect("device sink");
                assert_eq!(
                    sink,
                    "pulsesink device=\"alsa_output.pci-0000_00_1f.3.analog-stereo\" buffer-time=350000 latency-time=100000 sync=true"
                );
            },
        );
    }

    #[test]
    #[serial]
    fn pick_sink_element_uses_default_sink_from_pactl_info() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "info" ]; then
  echo "Server String: /run/user/1000/pulse/native"
  echo "Default Sink: alsa_output.usb-DAC_1234-00.analog-stereo"
  exit 0
fi
exit 0
"#;
        with_fake_pactl(script, || {
            with_var("LESAVKA_AUDIO_SINK", None::<&str>, || {
                let sinks = list_pw_sinks();
                assert_eq!(
                    sinks,
                    vec![(
                        "alsa_output.usb-DAC_1234-00.analog-stereo".to_string(),
                        "DEFAULT".to_string()
                    )]
                );
                let sink = pick_sink_element(None, true).expect("pick sink");
                assert_eq!(
                    sink,
                    "pulsesink device=\"alsa_output.usb-DAC_1234-00.analog-stereo\" buffer-time=350000 latency-time=100000 sync=true"
                );
            });
        });
    }

    #[test]
    #[serial]
    fn bluetooth_sink_override_uses_more_headroom() {
        with_var(
            "LESAVKA_AUDIO_SINK",
            Some("bluez_output.80_C3_BA_76_26_AB.1"),
            || {
                let sink = pick_sink_element(None, true).expect("bluetooth sink");
                assert_eq!(
                    sink,
                    "pulsesink device=\"bluez_output.80_C3_BA_76_26_AB.1\" buffer-time=750000 latency-time=250000 sync=true"
                );
            },
        );
    }

    #[test]
    #[serial]
    fn pick_sink_element_falls_back_to_autoaudiosink_without_pactl_default() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "info" ]; then
  echo "Server String: /run/user/1000/pulse/native"
  echo "Default Source: alsa_input.usb-Mic_1234-00.analog-stereo"
  exit 0
fi
exit 0
"#;
        with_fake_pactl(script, || {
            with_var("LESAVKA_AUDIO_SINK", None::<&str>, || {
                assert!(
                    list_pw_sinks().is_empty(),
                    "no default sink should be parsed"
                );
                let sink = pick_sink_element(None, true).expect("fallback sink");
                assert_eq!(sink, "autoaudiosink");
            });
        });
    }

    #[test]
    #[serial]
    fn audio_out_new_and_push_are_stable_with_sink_override() {
        with_var("LESAVKA_AUDIO_SINK", Some("fakesink sync=false"), || {
            with_var("LESAVKA_AUDIO_GAIN", Some("2.5"), || {
                with_var("LESAVKA_TAP_AUDIO", Some("1"), || match AudioOut::new() {
                    Ok(out) => {
                        out.push(AudioPacket {
                            id: 0,
                            pts: 1_234,
                            data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC],
                            ..AudioPacket::default()
                        });
                        drop(out);
                    }
                    Err(err) => {
                        assert!(!err.to_string().trim().is_empty());
                    }
                });
            });
        });
    }

    #[test]
    #[serial]
    fn audio_out_new_returns_error_for_invalid_sink_override() {
        with_var(
            "LESAVKA_AUDIO_SINK",
            Some("definitely-not-a-real-gst-sink"),
            || {
                with_var("LESAVKA_TAP_AUDIO", None::<&str>, || {
                    let result = AudioOut::new();
                    assert!(
                        result.is_err(),
                        "invalid sink override must fail pipeline parsing"
                    );
                });
            },
        );
    }

    #[test]
    fn live_audio_buffer_leaves_pts_for_appsrc_timestamping() {
        let _ = gst::init();
        let timeline = std::sync::Mutex::new(AudioTimeline::default());
        let buffer = live_audio_buffer(
            AudioPacket {
                id: 0,
                pts: 42_666,
                data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC],
                ..AudioPacket::default()
            },
            &timeline,
        );

        assert_eq!(buffer.pts(), None);
        assert_eq!(timeline.lock().expect("timeline").packets, 1);
    }

    #[test]
    #[serial]
    fn downstream_audio_pipeline_keeps_working_aac_transport_with_gain_stage() {
        with_var("LESAVKA_AUDIO_GAIN", Some("3.25"), || {
            assert_eq!(audio_gain_from_env(), 3.25);
        });
        let desc = audio_output_pipeline_desc("fakesink sync=false", 3.25, false);
        assert!(desc.contains("aacparse ! avdec_aac"));
        assert!(desc.contains("volume name=remote_audio_gain volume=3.250"));
        assert!(desc.contains("level name=remote_audio_level"));
        assert!(desc.contains("fakesink sync=false"));
    }

    #[test]
    fn downstream_audio_pipeline_can_tee_a_debug_aac_tap() {
        let desc = audio_output_pipeline_desc("fakesink sync=false", 1.0, true);
        assert!(desc.contains("tee name=t"));
        assert!(desc.contains("filesink location=/tmp/lesavka-audio.aac"));
        assert!(desc.contains("volume name=remote_audio_gain volume=1.000"));
    }

    #[test]
    #[serial]
    fn audio_gain_env_defaults_and_clamps_for_soft_remote_audio() {
        with_var("LESAVKA_AUDIO_GAIN", None::<&str>, || {
            assert_eq!(audio_gain_from_env(), 2.0);
        });
        with_var("LESAVKA_AUDIO_GAIN", Some("99"), || {
            assert_eq!(audio_gain_from_env(), 8.0);
        });
        with_var("LESAVKA_AUDIO_GAIN", Some("-1"), || {
            assert_eq!(audio_gain_from_env(), 0.0);
        });
        with_var("LESAVKA_AUDIO_GAIN", Some("nope"), || {
            assert_eq!(audio_gain_from_env(), 2.0);
        });
    }

    #[test]
    fn audio_gain_control_reads_first_token_and_clamps() {
        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("gain.control");
        fs::write(&path, "4.500 nonce\n").expect("write gain");
        assert_eq!(read_audio_gain_control(&path), Some(4.5));

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

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

    #[test]
    fn audio_gain_control_sample_applies_only_changed_values() {
        let _ = gst::init();
        let volume = gst::ElementFactory::make("volume")
            .build()
            .expect("volume element");
        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("gain.control");
        let mut last_gain = None;

        fs::write(&path, "1.5\n").expect("write gain");
        assert_eq!(
            apply_audio_gain_control_sample(&path, &volume, &mut last_gain),
            Some(1.5)
        );
        assert_eq!(last_gain, Some(1.5));
        assert_eq!(volume.property::<f64>("volume"), 1.5);
        assert_eq!(
            apply_audio_gain_control_sample(&path, &volume, &mut last_gain),
            None
        );

        fs::write(&path, "2.25\n").expect("write changed gain");
        assert_eq!(
            apply_audio_gain_control_sample(&path, &volume, &mut last_gain),
            Some(2.25)
        );
        assert_eq!(volume.property::<f64>("volume"), 2.25);
    }

    #[test]
    fn audio_gain_parsing_and_formatting_are_stable() {
        assert_eq!(parse_audio_gain("3.5 ignored"), Some(3.5));
        assert_eq!(parse_audio_gain("99"), Some(MAX_AUDIO_GAIN));
        assert_eq!(parse_audio_gain("-1"), Some(0.0));
        assert_eq!(parse_audio_gain("nan"), None);
        assert_eq!(parse_audio_gain(""), None);
        assert_eq!(format_audio_gain_for_gst(2.1256), "2.126");
    }

    #[test]
    fn sink_override_escaping_and_pactl_ranking_are_stable() {
        assert_eq!(normalize_sink_override("autoaudiosink"), "autoaudiosink");
        assert_eq!(
            normalize_sink_override("fakesink sync=false"),
            "fakesink sync=false"
        );
        let normal = normalize_sink_override("alsa_output.pci");
        assert!(normal.contains("pulsesink device=\"alsa_output.pci\""));
        assert!(normal.contains("buffer-time=350000"));
        let bluetooth = pulsesink_device_element("bluez_output.headset");
        assert!(bluetooth.contains("buffer-time=750000"));
        let escaped = pulsesink_device_element("sink\\\"name");
        assert!(escaped.contains("sink\\\\\\\"name"));

        assert_eq!(
            parse_pactl_default_sink("Server: x\nDefault Sink:  my.default  \n"),
            Some("my.default".to_string())
        );
        assert_eq!(parse_pactl_default_sink("Default Sink:   \n"), None);
        let sinks = parse_pactl_short_sinks(
            "bad\n1 idle.sink module IDLE\n2 run.sink module RUNNING\n3 suspended.sink module SUSPENDED\n",
            Some("missing.default"),
        );
        assert_eq!(
            sinks[0],
            ("missing.default".to_string(), "DEFAULT".to_string())
        );
        assert_eq!(sinks[1], ("run.sink".to_string(), "RUNNING".to_string()));
        assert_eq!(sink_state_rank("RUNNING"), 0);
        assert_eq!(sink_state_rank("IDLE"), 1);
        assert_eq!(sink_state_rank("SUSPENDED"), 2);
        assert_eq!(sink_state_rank("UNKNOWN"), 3);

        let sinks = parse_pactl_short_sinks(
            "1 default.sink module IDLE\n2 other.sink module IDLE\n",
            Some("default.sink"),
        );
        assert_eq!(sinks[0], ("default.sink".to_string(), "IDLE".to_string()));
    }

    #[test]
    #[serial]
    fn list_pw_sinks_uses_default_when_short_sink_listing_fails() {
        let script = r#"#!/usr/bin/env sh
if [ "$1" = "info" ]; then
  echo "Default Sink: fallback.default"
  exit 0
fi
if [ "$1" = "list" ]; then
  exit 1
fi
exit 1
"#;
        with_fake_pactl(script, || {
            with_var("LESAVKA_AUDIO_SINK", None::<&str>, || {
                assert_eq!(
                    list_pw_sinks(),
                    vec![("fallback.default".to_string(), "DEFAULT".to_string())]
                );
            });
        });
    }

    #[test]
    fn live_audio_buffer_logs_periodic_packets_after_remote_gap() {
        let _ = gst::init();
        let timeline = std::sync::Mutex::new(AudioTimeline {
            last_remote_pts_us: Some(1_000),
            packets: 599,
        });
        let buffer = live_audio_buffer(
            AudioPacket {
                id: 0,
                pts: 1_500,
                data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC],
                ..AudioPacket::default()
            },
            &timeline,
        );
        assert_eq!(buffer.size(), 7);
        let timeline = timeline.lock().expect("timeline");
        assert_eq!(timeline.last_remote_pts_us, Some(1_500));
        assert_eq!(timeline.packets, 600);
    }
}
