// Integration coverage for server process startup behavior.
//
// Scope: launch the real `lesavka-server` binary and assert startup stays
// resilient in this non-gadget test environment.
// Targets: `server/src/main.rs`.
// Why: missing gadget endpoints should not crash the relay; the server keeps
// running and opens HID lazily when the device nodes appear.

use serial_test::serial;
use std::fs;
use std::io::Read;
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::time::{Duration, Instant};

fn cargo_binary(name: &str) -> Option<PathBuf> {
    let key = format!("CARGO_BIN_EXE_{name}");
    option_env!("CARGO_BIN_EXE_lesavka-server")
        .filter(|_| name == "lesavka-server")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os(key).map(PathBuf::from))
        .filter(|path| path.exists() && path.is_file())
}

fn candidate_dirs() -> Vec<PathBuf> {
    let exe = std::env::current_exe().expect("current exe path");
    let mut dirs = Vec::new();
    if let Some(parent) = exe.parent() {
        dirs.push(parent.to_path_buf());
        if let Some(grand) = parent.parent() {
            dirs.push(grand.to_path_buf());
        }
    }
    dirs.push(PathBuf::from("target/debug"));
    dirs.push(PathBuf::from("target/llvm-cov-target/debug"));
    dirs
}

fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn build_current_binary(name: &str) -> Option<PathBuf> {
    if name != "lesavka-server" {
        return None;
    }
    static SERVER_BINARY: OnceLock<Option<PathBuf>> = OnceLock::new();
    SERVER_BINARY
        .get_or_init(|| {
            let cargo = option_env!("CARGO").unwrap_or("cargo");
            let target_dir = workspace_root().join("target/process-contract-debug");
            let _ = fs::remove_dir_all(&target_dir);
            let mut command = Command::new(cargo);
            let status = command
                .current_dir(workspace_root())
                .env_remove("CARGO_BUILD_RUSTFLAGS")
                .env_remove("CARGO_ENCODED_RUSTFLAGS")
                .env_remove("CARGO_LLVM_COV")
                .env_remove("CARGO_LLVM_COV_TARGET_DIR")
                .env_remove("CARGO_TARGET_DIR")
                .env_remove("LLVM_COV")
                .env_remove("RUSTFLAGS")
                .env_remove("RUSTDOCFLAGS")
                .env_remove("LLVM_PROFILE_FILE")
                .arg("build")
                .arg("--target-dir")
                .arg(&target_dir)
                .args(["-p", "lesavka_server", "--bin", name])
                .status()
                .ok()?;
            status
                .success()
                .then(|| target_dir.join("debug").join(name))
                .filter(|path| path.exists() && path.is_file())
        })
        .clone()
}

fn find_binary(name: &str) -> Option<PathBuf> {
    cargo_binary(name)
        .or_else(|| {
            candidate_dirs()
                .into_iter()
                .map(|dir| dir.join(name))
                .find(|path| path.exists() && path.is_file())
        })
        .or_else(|| build_current_binary(name))
}

fn server_package_version() -> Option<String> {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("server/Cargo.toml");
    fs::read_to_string(manifest).ok()?.lines().find_map(|line| {
        let trimmed = line.trim();
        trimmed
            .strip_prefix("version")
            .and_then(|value| value.split('=').nth(1))
            .map(str::trim)
            .and_then(|value| value.strip_prefix('"')?.strip_suffix('"'))
            .map(str::to_string)
    })
}

fn wait_for_log(path: &PathBuf, needle: &str, deadline: Instant) -> String {
    loop {
        let log = fs::read_to_string(path).unwrap_or_default();
        if log.contains(needle) {
            return log;
        }
        assert!(
            Instant::now() < deadline,
            "timed out waiting for log line {needle:?}; log was:\n{log}"
        );
        std::thread::sleep(Duration::from_millis(50));
    }
}

#[test]
#[serial]
fn server_binary_reports_version_without_starting_relay() {
    let Some(bin) = find_binary("lesavka-server") else {
        return;
    };
    let output = Command::new(bin)
        .arg("--version")
        .output()
        .expect("run lesavka-server --version");
    assert!(
        output.status.success(),
        "version probe should exit successfully; stderr was:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    if let Some(version) = server_package_version() {
        assert!(
            stdout.contains(&format!("lesavka_server {version}")),
            "version probe should print the package version; stdout was:\n{stdout}"
        );
    }
}

#[test]
#[serial]
fn server_binary_rejects_truncated_version_flag_without_starting_relay() {
    let Some(bin) = find_binary("lesavka-server") else {
        return;
    };
    let output = Command::new(bin)
        .arg("--versio")
        .output()
        .expect("run lesavka-server --versio");
    assert!(
        !output.status.success(),
        "truncated flag should fail instead of starting a second relay"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("unknown option")
            && stderr.contains("did you mean --version")
            && stderr.contains("Usage: lesavka-server"),
        "truncated flag should produce a helpful CLI error; stderr was:\n{stderr}"
    );
    assert!(
        !stderr.contains("Address already in use") && !stderr.contains("open singleton lock"),
        "argument validation should happen before relay, log, or UVC startup; stderr was:\n{stderr}"
    );
}

#[test]
#[serial]
fn server_binary_stays_up_with_missing_hid_nodes_and_current_version() {
    let Some(bin) = find_binary("lesavka-server") else {
        return;
    };
    let log_path = PathBuf::from("/tmp/lesavka-server.log");
    let _ = fs::remove_file(&log_path);
    let listener = TcpListener::bind("127.0.0.1:0").expect("reserve local test port");
    let bind_addr = listener.local_addr().expect("local test addr");
    drop(listener);

    if cfg!(coverage) {
        let mut child = Command::new(bin)
            .env("LESAVKA_DISABLE_UVC", "1")
            .env("LESAVKA_SERVER_BIND_ADDR", bind_addr.to_string())
            .stderr(Stdio::piped())
            .spawn()
            .expect("spawn coverage-mode lesavka-server");
        let deadline = Instant::now() + Duration::from_secs(3);
        loop {
            if let Some(status) = child.try_wait().expect("poll coverage-mode server") {
                let mut stderr = String::new();
                if let Some(mut pipe) = child.stderr.take() {
                    let _ = pipe.read_to_string(&mut stderr);
                }
                assert!(
                    !status.success(),
                    "coverage-mode server should not report success before the live-loop proof"
                );
                assert!(
                    stderr.contains("coverage mode skips live gRPC serve loop"),
                    "coverage-mode server should report the intentional live-loop skip; stderr was:\n{stderr}"
                );
                return;
            }

            let log = fs::read_to_string(&log_path).unwrap_or_default();
            if log.contains(
                "HID endpoints are not ready; relay will keep running and open them lazily",
            ) {
                let _ = child.kill();
                let _ = child.wait();
                return;
            }
            assert!(
                Instant::now() < deadline,
                "timed out waiting for coverage stub exit or live startup log; log was:\n{log}"
            );
            std::thread::sleep(Duration::from_millis(50));
        }
    }

    let mut child = Command::new(bin)
        .env("LESAVKA_DISABLE_UVC", "1")
        .env("LESAVKA_SERVER_BIND_ADDR", bind_addr.to_string())
        .spawn()
        .expect("spawn lesavka-server");

    let deadline = Instant::now() + Duration::from_secs(3);
    if let Some(version) = server_package_version() {
        let _ = wait_for_log(
            &log_path,
            &format!("lesavka_server v{version} starting up"),
            deadline,
        );
    }
    let log = wait_for_log(
        &log_path,
        "HID endpoints are not ready; relay will keep running and open them lazily",
        deadline,
    );
    assert!(
        child.try_wait().expect("poll child").is_none(),
        "server should stay alive with lazy HID recovery; log was:\n{log}"
    );
    let _ = child.kill();
    let _ = child.wait();
}
