// Integration coverage for the UVC runtime supervision contract.
//
// Scope: exercise the long-running UVC helper supervisor in both successful
// spawn and spawn-failure modes.
// Targets: `server/src/uvc_runtime.rs`.
// Why: the helper supervisor is operationally critical and should be covered
// through top-level integration behavior, not only unit checks.

use lesavka_server::uvc_runtime::{pick_uvc_device, supervise_uvc_control};
use serial_test::serial;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::time::Duration;
use temp_env::{with_var, with_vars};
use tempfile::tempdir;
use tokio::runtime::Runtime;

async fn wait_for_marker(marker: &Path, is_ready: impl Fn(&str) -> bool) -> String {
    let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
    loop {
        if let Ok(contents) = fs::read_to_string(marker)
            && is_ready(&contents)
        {
            return contents;
        }
        assert!(
            tokio::time::Instant::now() < deadline,
            "helper marker was not written in time"
        );
        tokio::time::sleep(Duration::from_millis(20)).await;
    }
}

#[test]
#[serial]
fn supervise_uvc_control_starts_helper_when_device_is_set() {
    let dir = tempdir().expect("create temp dir");
    let marker = dir.path().join("marker.log");
    let helper = dir.path().join("helper.sh");
    fs::write(
        &helper,
        format!(
            "#!/usr/bin/env bash\nset -euo pipefail\necho \"$*\" >> '{}'\nsleep 0.05\n",
            marker.display()
        ),
    )
    .expect("write helper script");
    let mut perms = fs::metadata(&helper)
        .expect("helper metadata")
        .permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&helper, perms).expect("chmod helper script");

    let helper_path = helper.to_string_lossy().to_string();
    let calls = with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || {
        let rt = Runtime::new().expect("runtime");
        rt.block_on(async {
            let supervisor = tokio::spawn(supervise_uvc_control(helper_path));
            let calls = wait_for_marker(&marker, |contents| {
                contents.contains("--device /dev/video-loop")
            })
            .await;
            supervisor.abort();
            let _ = supervisor.await;
            tokio::time::sleep(Duration::from_millis(100)).await;
            calls
        })
    });

    assert!(
        calls.contains("--device /dev/video-loop"),
        "expected helper to receive device args, got: {calls}"
    );
}

#[test]
#[serial]
fn supervise_uvc_control_restarts_helper_after_exit() {
    let dir = tempdir().expect("create temp dir");
    let marker = dir.path().join("marker.log");
    let helper = dir.path().join("helper.sh");
    fs::write(
        &helper,
        format!(
            "#!/usr/bin/env bash\nset -euo pipefail\necho \"$*\" >> '{}'\nsleep 0.05\nexit 0\n",
            marker.display()
        ),
    )
    .expect("write helper script");
    let mut perms = fs::metadata(&helper)
        .expect("helper metadata")
        .permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&helper, perms).expect("chmod helper script");

    let helper_path = helper.to_string_lossy().to_string();
    let calls = with_vars(
        [
            ("LESAVKA_UVC_DEV", Some("/dev/video-loop")),
            ("LESAVKA_UVC_RESTART_DELAY_MS", Some("25")),
        ],
        || {
            let rt = Runtime::new().expect("runtime");
            rt.block_on(async {
                let supervisor = tokio::spawn(supervise_uvc_control(helper_path));
                let calls = wait_for_marker(&marker, |contents| {
                    contents
                        .lines()
                        .filter(|line| line.contains("--device /dev/video-loop"))
                        .count()
                        >= 2
                })
                .await;
                supervisor.abort();
                let _ = supervisor.await;
                tokio::time::sleep(Duration::from_millis(100)).await;
                calls
            })
        },
    );

    let restart_count = calls
        .lines()
        .filter(|line| line.contains("--device /dev/video-loop"))
        .count();
    assert!(
        restart_count >= 2,
        "expected helper restart loop to run at least twice, got {restart_count}: {calls}"
    );
}

#[test]
#[serial]
fn supervise_uvc_control_survives_missing_helper_binary() {
    with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || {
        let rt = Runtime::new().expect("runtime");
        let result = rt.block_on(async {
            tokio::time::timeout(
                Duration::from_millis(350),
                supervise_uvc_control(String::from("/definitely/missing/lesavka-uvc")),
            )
            .await
        });
        assert!(
            result.is_err(),
            "supervisor should continue retrying forever"
        );
    });
}

#[test]
#[serial]
fn pick_uvc_device_prefers_controller_by_path_override_root() {
    let dir = tempdir().expect("tempdir");
    let sys_root = dir.path().join("sys");
    let by_path = dir.path().join("v4l/by-path");
    fs::create_dir_all(sys_root.join("class/udc/fake-ctrl.usb")).expect("create fake udc");
    fs::create_dir_all(&by_path).expect("create by-path dir");
    let expected = by_path.join("platform-fake-ctrl.usb-video-index0");
    fs::write(&expected, "").expect("touch by-path node");

    temp_env::with_var(
        "LESAVKA_GADGET_SYSFS_ROOT",
        Some(sys_root.to_string_lossy().to_string()),
        || {
            temp_env::with_var(
                "LESAVKA_UVC_BY_PATH_ROOT",
                Some(by_path.to_string_lossy().to_string()),
                || {
                    temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
                        temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
                            let picked = pick_uvc_device().expect("pick by-path device");
                            assert_eq!(picked, expected.to_string_lossy());
                        });
                    });
                },
            );
        },
    );
}

#[test]
#[serial]
fn pick_uvc_device_uses_platform_by_path_when_controller_is_unknown() {
    let dir = tempdir().expect("tempdir");
    let sys_root = dir.path().join("sys");
    let by_path = dir.path().join("v4l/by-path");
    fs::create_dir_all(&sys_root).expect("create fake sys root");
    fs::create_dir_all(&by_path).expect("create fake by-path root");
    let expected = by_path.join("platform-fallback.usb-video-index0");
    fs::write(&expected, "").expect("touch by-path node");

    temp_env::with_var(
        "LESAVKA_GADGET_SYSFS_ROOT",
        Some(sys_root.to_string_lossy().to_string()),
        || {
            temp_env::with_var(
                "LESAVKA_UVC_BY_PATH_ROOT",
                Some(by_path.to_string_lossy().to_string()),
                || {
                    temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
                        temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
                            let picked = pick_uvc_device().expect("pick platform by-path device");
                            assert_eq!(picked, expected.to_string_lossy());
                        });
                    });
                },
            );
        },
    );
}

#[test]
#[serial]
fn pick_uvc_device_errors_when_overrides_disable_all_discovery_paths() {
    let dir = tempdir().expect("tempdir");
    let sys_root = dir.path().join("sys");
    let by_path = dir.path().join("v4l/by-path");
    fs::create_dir_all(&sys_root).expect("create fake sys root");
    fs::create_dir_all(&by_path).expect("create fake by-path root");

    temp_env::with_var(
        "LESAVKA_GADGET_SYSFS_ROOT",
        Some(sys_root.to_string_lossy().to_string()),
        || {
            temp_env::with_var(
                "LESAVKA_UVC_BY_PATH_ROOT",
                Some(by_path.to_string_lossy().to_string()),
                || {
                    temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
                        temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
                            let err = pick_uvc_device().expect_err("missing paths should error");
                            assert!(
                                err.to_string().contains("LESAVKA_UVC_DEV"),
                                "error should mention explicit override escape hatch: {err:#}"
                            );
                            assert!(
                                err.to_string().contains("platform-<udc>-video-index0"),
                                "error should point operators at the gadget by-path node: {err:#}"
                            );
                        });
                    });
                },
            );
        },
    );
}
