use super::AudioDevice;
use std::process::Command;

pub fn enumerate_audio_devices() -> Vec<AudioDevice> {
    let output = Command::new("pw-cli")
        .arg("list-objects")
        .arg("Node")
        .output();

    match output {
        Ok(out) => parse_pw_nodes(&String::from_utf8_lossy(&out.stdout)),
        Err(_) => Vec::new(),
    }
}

/// Emits the in-progress node as an `AudioDevice` if it's a complete Audio/*
/// node, then resets the accumulators for the next block. Non-audio or
/// incomplete blocks are dropped (but still reset).
fn push_device(
    name: &mut String,
    desc: &mut String,
    class: &mut String,
    out: &mut Vec<AudioDevice>,
) {
    if !name.is_empty() && class.starts_with("Audio/") {
        out.push(AudioDevice {
            name: name.clone(),
            description: if desc.is_empty() {
                name.clone()
            } else {
                desc.clone()
            },
            is_input: class == "Audio/Source",
        });
    }
    name.clear();
    desc.clear();
    class.clear();
}

/// Parses the text of `pw-cli list-objects Node` into the audio devices we care
/// about. Each object is a block introduced by an `id N, ...` line; within a
/// block we collect `node.name` / `node.description` / `media.class`, and a
/// device is emitted at the next `id` (and at EOF) when the class is `Audio/*`
/// (`Audio/Source` => input). Description falls back to the node name when
/// absent. Returned sorted by description for stable UI display.
fn parse_pw_nodes(text: &str) -> Vec<AudioDevice> {
    let mut devices = Vec::new();
    let mut current_name = String::new();
    let mut current_desc = String::new();
    let mut current_class = String::new();

    for line in text.lines() {
        let line = line.trim();
        if line.starts_with("id ") {
            push_device(
                &mut current_name,
                &mut current_desc,
                &mut current_class,
                &mut devices,
            );
        } else if let Some(val) = line.strip_prefix("node.name = \"") {
            current_name = val.trim_end_matches('"').to_string();
        } else if let Some(val) = line.strip_prefix("node.description = \"") {
            current_desc = val.trim_end_matches('"').to_string();
        } else if let Some(val) = line.strip_prefix("media.class = \"") {
            current_class = val.trim_end_matches('"').to_string();
        }
    }
    push_device(
        &mut current_name,
        &mut current_desc,
        &mut current_class,
        &mut devices,
    );

    devices.sort_by(|a, b| a.description.cmp(&b.description));
    devices
}

#[cfg(test)]
mod tests {
    use super::*;

    const SAMPLE_NODES: &str = r#"
    id 33, type PipeWire:Interface:Node/3
        node.name = "alsa_output.builtin"
        node.description = "Built-in Speakers"
        media.class = "Audio/Sink"
    id 34, type PipeWire:Interface:Node/3
        node.name = "alsa_input.builtin"
        node.description = "Built-in Mic"
        media.class = "Audio/Source"
    id 35, type PipeWire:Interface:Node/3
        node.name = "v4l2.cam"
        media.class = "Video/Source"
    id 36, type PipeWire:Interface:Node/3
        node.name = "bare.sink"
        media.class = "Audio/Sink"
    "#;

    #[test]
    fn parses_audio_nodes_sorted_by_description() {
        let devices = parse_pw_nodes(SAMPLE_NODES);

        // exactly 3 devices (the Video/Source node is dropped)
        assert_eq!(devices.len(), 3);

        // order is by description: ["Built-in Mic", "Built-in Speakers", "bare.sink"]
        // (uppercase "B" sorts before lowercase "b")
        assert_eq!(devices[0].description, "Built-in Mic");
        assert_eq!(devices[1].description, "Built-in Speakers");
        assert_eq!(devices[2].description, "bare.sink");

        // the mic device equals:
        assert_eq!(
            devices[0],
            AudioDevice {
                name: "alsa_input.builtin".into(),
                description: "Built-in Mic".into(),
                is_input: true,
            }
        );
    }

    #[test]
    fn source_is_input_sink_is_output() {
        let devices = parse_pw_nodes(SAMPLE_NODES);
        // Find devices by name or description to verify is_input
        let mic = devices
            .iter()
            .find(|d| d.name == "alsa_input.builtin")
            .unwrap();
        let speakers = devices
            .iter()
            .find(|d| d.name == "alsa_output.builtin")
            .unwrap();
        let bare = devices.iter().find(|d| d.name == "bare.sink").unwrap();

        assert!(mic.is_input);
        assert!(!speakers.is_input);
        assert!(!bare.is_input);
    }

    #[test]
    fn description_falls_back_to_node_name() {
        let devices = parse_pw_nodes(SAMPLE_NODES);
        let bare = devices.iter().find(|d| d.name == "bare.sink").unwrap();
        assert_eq!(bare.description, "bare.sink");
    }

    #[test]
    fn non_audio_and_empty_inputs_yield_nothing() {
        assert!(parse_pw_nodes("").is_empty());

        let video_sample = r#"
        id 35, type PipeWire:Interface:Node/3
            node.name = "v4l2.cam"
            media.class = "Video/Source"
        "#;
        assert!(parse_pw_nodes(video_sample).is_empty());
    }

    #[test]
    fn final_block_is_emitted_at_eof() {
        let single_sample = r#"
        id 99, type PipeWire:Interface:Node/3
            node.name = "single.device"
            media.class = "Audio/Sink"
        "#;
        let devices = parse_pw_nodes(single_sample);
        assert_eq!(devices.len(), 1);
        assert_eq!(devices[0].name, "single.device");
    }

    #[test]
    fn incomplete_block_without_class_is_dropped() {
        let incomplete_sample = r#"
        id 100, type PipeWire:Interface:Node/3
            node.name = "incomplete.device"
            node.description = "Incomplete Device"
        "#;
        let devices = parse_pw_nodes(incomplete_sample);
        assert!(devices.is_empty());
    }
}
