//! Test support for tail-fin-daemon integration tests.
//!
//! `MockDriver` implements `SessionDriver` without touching any browser or
//! network. It returns deterministic `Response` values so integration tests
//! can exercise the server's command-dispatch, pool, and keepalive paths.

use std::time::Duration;

use night_fury_daemon_core::{protocol::Response, LaunchError, SessionDriver};
use serde_json::{json, Value};

/// A deterministic, in-memory `SessionDriver` for integration tests.
///
/// Launch params:
/// - `site`, `host` — stored verbatim, echoed back in responses
/// - `fail_handle` — if true, `handle()` returns an error Response (for
///   dead-session eviction tests)
pub struct MockDriver {
    pub site: String,
    pub host: String,
    pub fail_handle: bool,
}

impl SessionDriver for MockDriver {
    async fn launch(params: Value) -> Result<Self, LaunchError> {
        let site = params
            .get("site")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        let host = params
            .get("host")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        let fail_handle = params
            .get("fail_handle")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        Ok(Self {
            site,
            host,
            fail_handle,
        })
    }

    async fn handle(&self, id: &str, cmd: &str, _params: &Value) -> (Response, bool) {
        if self.fail_handle {
            (Response::err(id, "mock driver: intentional failure"), false)
        } else {
            (
                Response::ok(
                    id,
                    json!({
                        "mock_ok": true,
                        "cmd": cmd,
                        "site": self.site,
                        "host": self.host,
                    }),
                ),
                false,
            )
        }
    }
}

/// Spawn `serve_with_driver::<MockDriver>` as a background task on a unique
/// socket path under the given tempdir. Returns the socket path once the
/// listener is ready, plus the spawned task handle so the caller can abort/await.
pub async fn spawn_mock_daemon(
    sock_dir: &std::path::Path,
    idle_timeout_secs: u64,
    daemon_idle_secs: u64,
    max_per_pool: usize,
) -> (String, tokio::task::JoinHandle<()>) {
    let sock = sock_dir
        .join(format!("tfd-{}.sock", uuid_short()))
        .to_string_lossy()
        .to_string();
    let sock_bg = sock.clone();
    let handle = tokio::spawn(async move {
        let _ = tail_fin_daemon::server::serve_with_driver::<MockDriver>(
            &sock_bg,
            idle_timeout_secs,
            daemon_idle_secs,
            max_per_pool,
        )
        .await;
    });

    wait_for_socket(&sock).await;
    (sock, handle)
}

/// Like `spawn_mock_daemon` but with an explicit keepalive period. Uses
/// the test-only `serve_with_driver_keepalive` entry point so tests can
/// exercise the keepalive + reaper interaction in bounded time.
#[allow(dead_code)]
pub async fn spawn_mock_daemon_with_keepalive(
    sock_dir: &std::path::Path,
    idle_timeout_secs: u64,
    daemon_idle_secs: u64,
    max_per_pool: usize,
    keepalive_period: Duration,
) -> (String, tokio::task::JoinHandle<()>) {
    let sock = sock_dir
        .join(format!("tfd-{}.sock", uuid_short()))
        .to_string_lossy()
        .to_string();
    let sock_bg = sock.clone();
    let handle = tokio::spawn(async move {
        let _ = tail_fin_daemon::server::serve_with_options::<MockDriver>(
            &sock_bg,
            idle_timeout_secs,
            daemon_idle_secs,
            max_per_pool,
            /* reap_interval */ std::time::Duration::from_secs(1),
            keepalive_period,
        )
        .await;
    });

    wait_for_socket(&sock).await;
    (sock, handle)
}

async fn wait_for_socket(sock: &str) {
    for _ in 0..50 {
        if tokio::net::UnixStream::connect(sock).await.is_ok() {
            return;
        }
        tokio::time::sleep(Duration::from_millis(100)).await;
    }
    panic!("daemon did not start within 5s at {sock}");
}

fn uuid_short() -> String {
    // Full v4 UUID is collision-proof regardless of clock resolution.
    // We shorten to 8 chars for readable socket filenames.
    let u = uuid::Uuid::new_v4().to_string();
    u[..8].to_string()
}
