use super::*;
use std::path::PathBuf;

fn mk_temp_dir(prefix: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    path.push(format!("lesavka-{prefix}-{}-{nanos}", std::process::id()));
    std::fs::create_dir_all(&path).expect("create temp dir");
    path
}

#[test]
fn parse_pactl_short_collects_second_column_and_sorts_unique() {
    let input = "0 alsa_input.usb.test module-x\n1 alsa_input.usb.test module-x\n2 alsa_input.pci module-y\n";
    let parsed = parse_pactl_short(input);
    assert_eq!(
        parsed,
        vec![
            "alsa_input.pci".to_string(),
            "alsa_input.usb.test".to_string(),
        ]
    );
}

#[test]
fn parse_pactl_short_ignores_blank_or_short_lines() {
    let input = "\nweird\n3\n4 sink.a\tmodule\n";
    let parsed = parse_pactl_short(input);
    assert_eq!(parsed, vec!["sink.a".to_string()]);
}

#[test]
fn camera_discovery_reads_entry_names_from_override_dir() {
    let tmp = mk_temp_dir("camera-discovery");
    std::fs::write(tmp.join("usb-cam-a"), "").expect("write");
    std::fs::write(tmp.join("usb-cam-b"), "").expect("write");

    let devices = discover_camera_devices(Some(tmp.to_string_lossy().to_string()));
    assert_eq!(
        devices,
        vec!["usb-cam-a".to_string(), "usb-cam-b".to_string()]
    );
    let _ = std::fs::remove_dir_all(tmp);
}

#[test]
fn camera_discovery_prefers_one_endpoint_per_physical_webcam() {
    let devices = dedupe_camera_devices([
        "usb-Azurewave_USB2.0_HD_UVC_WebCam_0x0001-video-index1".to_string(),
        "usb-Azurewave_USB2.0_HD_UVC_WebCam_0x0001-video-index0".to_string(),
        "usb-Logitech_C920-video-index0".to_string(),
    ]);

    assert_eq!(
        devices,
        vec![
            "usb-Azurewave_USB2.0_HD_UVC_WebCam_0x0001-video-index0".to_string(),
            "usb-Logitech_C920-video-index0".to_string(),
        ]
    );
}

#[test]
fn camera_discovery_returns_empty_when_directory_missing() {
    let devices = discover_camera_devices(Some("/tmp/does-not-exist-lesavka".to_string()));
    assert!(devices.is_empty());
}

#[test]
fn camera_discovery_default_path_is_stable_without_overrides() {
    let _ = discover_camera_devices(None);
}

#[test]
fn camera_mode_parser_keeps_only_supported_lesavka_qualities() {
    let stdout = r#"
ioctl: VIDIOC_ENUM_FMT
    Type: Video Capture

    [0]: 'MJPG' (Motion-JPEG, compressed)
            Size: Discrete 1920x1080
                    Interval: Discrete 0.033s (30.000 fps)
                    Interval: Discrete 0.050s (20.000 fps)
                    Interval: Discrete 0.067s (15.000 fps)
            Size: Discrete 1280x720
                    Interval: Discrete 0.017s (60.000 fps)
                    Interval: Discrete 0.033s (30.000 fps)
                    Interval: Discrete 0.050s (20.000 fps)
            Size: Discrete 640x480
                    Interval: Discrete 0.033s (30.000 fps)
    [1]: 'YUYV' (YUYV 4:2:2)
            Size: Discrete 1920x1080
                    Interval: Discrete 0.200s (5.000 fps)
"#;

    assert_eq!(
        parse_supported_camera_modes(stdout),
        vec![
            CameraMode::new(1920, 1080, 30),
            CameraMode::new(1920, 1080, 20),
            CameraMode::new(1280, 720, 30),
            CameraMode::new(1280, 720, 20)
        ]
    );
}

#[test]
fn camera_mode_parser_requires_exact_fps_for_supported_qualities() {
    let stdout = r#"
ioctl: VIDIOC_ENUM_FMT
    Type: Video Capture

    [0]: 'MJPG' (Motion-JPEG, compressed)
            Size: Discrete 1280x720
                    Interval: Discrete 0.033s (30.000 fps)
            Size: Discrete 1920x1080
                    Interval: Discrete 0.017s (60.000 fps)
"#;

    assert_eq!(
        parse_supported_camera_modes(stdout),
        vec![CameraMode::new(1280, 720, 30)]
    );
}

#[test]
fn supported_camera_modes_match_the_core_logitech_profiles() {
    assert_eq!(
        lesavka_supported_camera_modes(),
        vec![
            CameraMode::new(1920, 1080, 30),
            CameraMode::new(1920, 1080, 20),
            CameraMode::new(1280, 720, 30),
            CameraMode::new(1280, 720, 20)
        ]
    );
}

#[test]
fn camera_mode_ids_are_short_and_round_trippable() {
    let mode = CameraMode::new(1920, 1080, 30);
    assert_eq!(mode.id(), "1920x1080@30");
    assert_eq!(mode.short_label(), "1080p@30");
    assert_eq!(mode.h264_bitrate_kbit(), 12_000);
    assert_eq!(CameraMode::new(1280, 720, 30).h264_bitrate_kbit(), 6_000);
    assert_eq!(CameraMode::new(640, 480, 30).h264_bitrate_kbit(), 3_000);
    assert_eq!(CameraMode::from_id("1920x1080@30"), Some(mode));
    assert_eq!(CameraMode::from_id("not-a-mode"), None);
    assert_eq!(CameraMode::from_id("1920x1080@0"), None);
}

#[test]
fn camera_device_rank_prefers_primary_video_endpoint() {
    assert_eq!(camera_device_rank("usb-Cam-video-index0"), 0);
    assert_eq!(camera_device_rank("usb-Cam-video-index1"), 1);
    assert_eq!(camera_device_rank("usb-Cam-without-index"), 2);
}

#[test]
fn direct_camera_mode_probe_tolerates_missing_or_bad_device() {
    assert!(discover_camera_modes_for("/dev/lesavka-definitely-missing").is_empty());
}

#[test]
fn discover_uses_override_and_tolerates_missing_pactl() {
    let tmp = mk_temp_dir("discover-override");
    std::fs::write(tmp.join("cam"), "").expect("write");
    let catalog =
        DeviceCatalog::discover_with_camera_override(Some(tmp.to_string_lossy().to_string()));
    assert_eq!(catalog.cameras, vec!["cam".to_string()]);
    let _ = std::fs::remove_dir_all(tmp);
}

#[test]
fn discover_is_stable_with_process_environment_defaults() {
    let _ = DeviceCatalog::discover();
}

#[test]
fn catalog_empty_reflects_collections() {
    let mut catalog = DeviceCatalog::default();
    assert!(catalog.is_empty());
    catalog.speakers.push("sink-1".to_string());
    assert!(!catalog.is_empty());
}

#[test]
fn parse_pipewire_audio_nodes_collects_named_audio_nodes() {
    let sample = br#"
[
  {
"info": {
  "props": {
    "media.class": "Audio/Source",
    "node.name": "alsa_input.usb-TestMic-00.mono-fallback"
  }
}
  },
  {
"info": {
  "props": {
    "media.class": "Audio/Source",
    "node.name": "bluez_input.80:C3:BA:76:26:AB"
  }
}
  },
  {
"info": {
  "props": {
    "media.class": "Audio/Sink",
    "node.name": "alsa_output.pci-0000_00_1f.3.analog-stereo"
  }
}
  }
]
"#;
    assert_eq!(
        parse_pipewire_audio_nodes(sample, "Audio/Source"),
        vec![
            "alsa_input.usb-TestMic-00.mono-fallback".to_string(),
            "bluez_input.80:C3:BA:76:26:AB".to_string(),
        ]
    );
}

#[test]
fn parse_pipewire_audio_nodes_ignores_invalid_payloads() {
    assert!(parse_pipewire_audio_nodes(b"not json", "Audio/Source").is_empty());
}

#[test]
fn parse_pipewire_audio_nodes_ignores_objects_without_names() {
    let sample = br#"
[
  {"info":{"props":{"media.class":"Audio/Source"}}},
  {"info":{"props":{"media.class":"Audio/Source","node.nick":"Nick Source"}}},
  {"info":{"props":{"media.class":"Audio/Sink","device.name":"Sink Name"}}}
]
"#;
    assert_eq!(
        parse_pipewire_audio_nodes(sample, "Audio/Source"),
        vec!["Nick Source".to_string()]
    );
    assert_eq!(
        parse_pipewire_audio_nodes(sample, "Audio/Sink"),
        vec!["Sink Name".to_string()]
    );
}
