use super::{
    allow_gadget_cycle, detect_uac_card_candidates, external_uvc_helper_owns_gadget, init_tracing,
    next_stream_id, open_ear_with_retry, open_hid_if_ready, open_with_retry,
    parse_uac_named_card_candidates, parse_uac_numeric_card_ids, parse_uac_pcm_candidates,
    preferred_uac_device_candidates, push_audio_candidate, push_audio_candidate_family,
    should_recover_hid_error, write_hid_report,
};
use serial_test::serial;
use std::collections::BTreeSet;
use std::sync::Arc;
use temp_env::with_var;
use tempfile::NamedTempFile;
use tempfile::tempdir;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;

#[test]
#[serial]
fn allow_gadget_cycle_tracks_env_presence() {
    with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
        assert!(!allow_gadget_cycle());
    });
    with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
        assert!(allow_gadget_cycle());
    });
}

#[test]
#[serial]
fn allow_gadget_cycle_defers_to_external_uvc_owner() {
    with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
        with_var("LESAVKA_UVC_EXTERNAL", Some("1"), || {
            with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
                with_var(
                    "LESAVKA_ALLOW_EXTERNAL_UVC_GADGET_CYCLE",
                    None::<&str>,
                    || {
                        assert!(external_uvc_helper_owns_gadget());
                        assert!(
                            !allow_gadget_cycle(),
                            "server must not reset the gadget while external UVC owns it"
                        );
                    },
                );
            });
        });
    });
}

#[test]
#[serial]
fn allow_gadget_cycle_can_be_forced_with_external_uvc_owner() {
    with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
        with_var("LESAVKA_UVC_EXTERNAL", Some("1"), || {
            with_var("LESAVKA_ALLOW_EXTERNAL_UVC_GADGET_CYCLE", Some("1"), || {
                assert!(allow_gadget_cycle());
            });
        });
    });
}

#[test]
fn should_recover_hid_error_matches_transport_failures() {
    assert!(should_recover_hid_error(Some(libc::ENOTCONN)));
    assert!(should_recover_hid_error(Some(libc::ESHUTDOWN)));
    assert!(should_recover_hid_error(Some(libc::EPIPE)));
    assert!(!should_recover_hid_error(Some(libc::EAGAIN)));
    assert!(!should_recover_hid_error(None));
}

#[test]
fn next_stream_id_monotonically_increments() {
    let first = next_stream_id();
    let second = next_stream_id();
    assert!(second > first);
}

#[test]
fn preferred_uac_device_candidates_keeps_custom_override_only() {
    let candidates = preferred_uac_device_candidates("hw:7,0");
    assert_eq!(candidates, vec!["hw:7,0", "plughw:7,0"]);
}

#[test]
fn preferred_uac_device_candidates_handles_blank_and_plughw_overrides() {
    assert!(preferred_uac_device_candidates("   ").is_empty());
    assert_eq!(
        preferred_uac_device_candidates(" plughw:8,2 "),
        vec!["plughw:8,2", "hw:8,2"]
    );
}

#[test]
fn preferred_uac_device_candidates_expands_known_aliases() {
    let candidates = preferred_uac_device_candidates("hw:UAC2Gadget,0");
    assert!(candidates.iter().any(|value| value == "hw:UAC2Gadget,0"));
    assert!(
        candidates
            .iter()
            .any(|value| value == "plughw:UAC2Gadget,0")
    );
    assert!(candidates.iter().any(|value| value == "hw:UAC2_Gadget,0"));
    assert!(candidates.iter().any(|value| value == "hw:Composite,0"));
}

#[test]
fn audio_candidate_helpers_dedupe_and_pair_hw_plughw_forms() {
    let mut out = Vec::new();
    let mut seen = BTreeSet::new();

    push_audio_candidate(&mut out, &mut seen, "   ");
    push_audio_candidate(&mut out, &mut seen, " hw:9,0 ");
    push_audio_candidate(&mut out, &mut seen, "hw:9,0");
    push_audio_candidate_family(&mut out, &mut seen, "plughw:9,0");
    push_audio_candidate_family(&mut out, &mut seen, "   ");

    assert_eq!(out, vec!["hw:9,0".to_string(), "plughw:9,0".to_string()]);
}

#[test]
#[cfg(coverage)]
#[serial]
fn preferred_uac_candidates_include_detected_cards_before_alias_fallbacks() {
    temp_env::with_vars(
        [
            (
                "LESAVKA_TEST_ASOUND_CARDS",
                Some(
                    " 7 [DetectedGadget ]: USB-Audio - UAC2 Gadget
\
                     8 [LesavkaAudio   ]: USB-Audio - Lesavka
",
                ),
            ),
            (
                "LESAVKA_TEST_ASOUND_PCM",
                Some(
                    "07-03: USB Audio : USB Audio : playback 1 : capture 1
",
                ),
            ),
        ],
        || {
            let candidates = preferred_uac_device_candidates("hw:UAC2Gadget,0");
            assert_eq!(candidates.first().map(String::as_str), Some("hw:7,3"));
            assert!(candidates.iter().any(|value| value == "plughw:7,3"));
            assert!(
                candidates
                    .iter()
                    .any(|value| value == "hw:DetectedGadget,0")
            );
            assert!(candidates.iter().any(|value| value == "hw:LesavkaAudio,0"));
            assert!(candidates.iter().any(|value| value == "hw:7,3"));
            let preferred_index = candidates
                .iter()
                .position(|value| value == "hw:UAC2Gadget,0")
                .expect("preferred alias present");
            let numeric_index = candidates
                .iter()
                .position(|value| value == "hw:7,3")
                .expect("numeric candidate present");
            let detected_index = candidates
                .iter()
                .position(|value| value == "hw:DetectedGadget,0")
                .expect("detected candidate present");
            assert!(numeric_index < detected_index);
            assert!(detected_index < preferred_index);
        },
    );
}

#[test]
fn detect_uac_card_candidates_returns_hw_names_only() {
    let live = detect_uac_card_candidates();
    assert!(live.iter().all(|value| value.starts_with("hw:")));
}

#[test]
fn parse_uac_card_helpers_collect_named_and_numeric_candidates() {
    let cards = "\
 0 [PCH            ]: HDA-Intel - HDA Intel PCH\n\
 2 [UAC2Gadget     ]: USB-Audio - UAC2Gadget\n\
                  Lesavka USB Audio\n";

    assert_eq!(
        parse_uac_named_card_candidates(cards),
        vec!["hw:UAC2Gadget,0"]
    );
    assert!(
        parse_uac_numeric_card_ids(cards).contains("2"),
        "expected numeric card index for the gadget card"
    );
}

#[test]
fn parse_uac_card_helpers_ignore_malformed_candidates() {
    let cards = "\
 XX [BrokenGadget  ]: USB-Audio - UAC2 Gadget\n\
 03 NoBracketGadget : USB-Audio - Lesavka\n\
 04 [              ]: USB-Audio - Composite\n\
 05 [Composite     ]: USB-Audio - Composite\n";

    assert_eq!(
        parse_uac_named_card_candidates(cards),
        vec!["hw:BrokenGadget,0", "hw:Composite,0"]
    );
    assert_eq!(
        parse_uac_numeric_card_ids(cards),
        BTreeSet::from(["03".to_string(), "04".to_string(), "05".to_string()])
    );
}

#[test]
fn parse_uac_pcm_candidates_expands_all_matching_device_indexes() {
    let pcm = "\
00-00: PCH device : playback 1 : capture 1\n\
02-00: USB Audio : USB Audio : playback 1 : capture 1\n\
02-01: USB Audio #1 : USB Audio #1 : playback 1 : capture 1\n";
    let ids = BTreeSet::from(["2".to_string()]);

    assert_eq!(
        parse_uac_pcm_candidates(pcm, &ids),
        vec!["hw:2,0", "hw:2,1"]
    );
}

#[test]
fn parse_uac_pcm_candidates_normalizes_zeroes_and_skips_non_matching_cards() {
    let pcm = "\
00-00: Zero card : playback 1 : capture 1\n\
09-03: Other card : playback 1 : capture 1\n\
bad line without separator\n";
    let ids = BTreeSet::from(["0".to_string()]);

    assert_eq!(parse_uac_pcm_candidates(pcm, &ids), vec!["hw:0,0"]);
}

#[tokio::test]
#[serial]
async fn open_with_retry_opens_existing_file() {
    let tmp = NamedTempFile::new().expect("temp file");
    let mut file = open_with_retry(tmp.path().to_str().unwrap())
        .await
        .expect("open should succeed");
    file.write_all(b"ok").await.expect("write temp file");
    file.sync_all().await.expect("sync temp file");
    assert_eq!(
        tokio::fs::read(tmp.path()).await.expect("read temp file"),
        b"ok"
    );
}

#[test]
fn init_tracing_returns_a_guard_under_coverage() {
    let _guard = init_tracing().expect("coverage tracing guard");
}

#[tokio::test]
#[serial]
async fn hid_open_helpers_return_contextual_errors_for_bad_paths() {
    let dir = tempdir().expect("tempdir");
    let err = open_with_retry(dir.path().to_str().unwrap())
        .await
        .expect_err("directory should not open as HID file");
    assert!(format!("{err:#}").contains("opening"));

    let err = open_hid_if_ready(dir.path().to_str().unwrap())
        .await
        .expect_err("directory should be a hard open error");
    assert!(format!("{err:#}").contains("opening"));
}

#[tokio::test]
#[serial]
async fn write_hid_report_writes_bytes() {
    let tmp = NamedTempFile::new().expect("temp file");
    let file = tokio::fs::OpenOptions::new()
        .write(true)
        .truncate(true)
        .open(tmp.path())
        .await
        .expect("open temp file");
    let shared = Arc::new(Mutex::new(Some(file)));

    write_hid_report(&shared, tmp.path().to_str().unwrap(), &[1, 2, 3, 4])
        .await
        .expect("write succeeds");

    let contents = tokio::fs::read(tmp.path())
        .await
        .expect("read back temp file");
    assert_eq!(&contents, &[1, 2, 3, 4]);
}

#[tokio::test]
#[serial]
async fn write_hid_report_opens_lazily_when_handle_is_empty() {
    let tmp = NamedTempFile::new().expect("temp file");
    let shared = Arc::new(Mutex::new(None));

    write_hid_report(&shared, tmp.path().to_str().unwrap(), &[9, 8])
        .await
        .expect("lazy write succeeds");

    let contents = tokio::fs::read(tmp.path())
        .await
        .expect("read back temp file");
    assert_eq!(&contents, &[9, 8]);
}

#[test]
#[serial]
fn open_ear_with_retry_reports_bad_capture_device() {
    let err = temp_env::with_vars(
        [
            ("LESAVKA_AUDIO_INIT_ATTEMPTS", Some("1")),
            ("LESAVKA_AUDIO_INIT_DELAY_MS", Some("0")),
        ],
        || {
            let runtime = tokio::runtime::Runtime::new().expect("test runtime");
            match runtime.block_on(open_ear_with_retry(
                "hw:DefinitelyMissingLesavkaDevice,99",
                99,
            )) {
                Ok(_) => panic!("missing ALSA source should fail"),
                Err(err) => err,
            }
        },
    );
    assert!(!format!("{err:#}").is_empty());
}
