// Integration coverage for the handshake and camera-selection contract.
//
// Scope: exercise the real client handshake against a local server and the
// server camera selection logic that feeds the handshake response.
// Targets: `client/src/handshake.rs`, `server/src/handshake.rs`, and
// `server/src/camera.rs`.
// Why: the handshake path is the narrow entrypoint that decides whether the
// client and server agree on output mode, so it deserves a centralized
// contract test.

use lesavka_client::handshake::{HandshakeProbe, PeerCaps, negotiate, probe};
use lesavka_common::lesavka::{
    Empty, HandshakeSet,
    handshake_server::{Handshake, HandshakeServer},
};
use serial_test::serial;
use std::net::{SocketAddr, TcpListener};
use std::time::Duration;
use temp_env::with_var;
use tokio::runtime::Runtime;
use tokio::sync::oneshot;
use tonic::{
    Request, Response, Status,
    transport::{Endpoint, Server},
};

async fn wait_for_handshake_endpoint(addr: SocketAddr) {
    let endpoint = Endpoint::from_shared(format!("http://{addr}"))
        .expect("local endpoint")
        .tcp_nodelay(true);
    for _ in 0..100 {
        if endpoint.clone().connect().await.is_ok() {
            return;
        }
        tokio::time::sleep(Duration::from_millis(25)).await;
    }
    panic!("local handshake server did not become reachable");
}

async fn negotiate_against_service<S>(service: S) -> PeerCaps
where
    S: Handshake + Send + Sync + 'static,
{
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind local handshake listener");
    let addr = listener.local_addr().expect("listener addr");
    drop(listener);

    let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
    let server = tokio::spawn(async move {
        Server::builder()
            .add_service(HandshakeServer::new(service))
            .serve_with_shutdown(addr, async move {
                let _ = shutdown_rx.await;
            })
            .await
            .expect("serve handshake server");
    });

    wait_for_handshake_endpoint(addr).await;
    let caps = negotiate(&format!("http://{addr}")).await;
    let _ = shutdown_tx.send(());
    let _ = server.await;
    caps
}

async fn probe_against_service<S>(service: S) -> HandshakeProbe
where
    S: Handshake + Send + Sync + 'static,
{
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind local handshake listener");
    let addr = listener.local_addr().expect("listener addr");
    drop(listener);

    let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
    let server = tokio::spawn(async move {
        Server::builder()
            .add_service(HandshakeServer::new(service))
            .serve_with_shutdown(addr, async move {
                let _ = shutdown_rx.await;
            })
            .await
            .expect("serve handshake server");
    });

    wait_for_handshake_endpoint(addr).await;
    let result = probe(&format!("http://{addr}")).await;
    let _ = shutdown_tx.send(());
    let _ = server.await;
    result
}

async fn negotiate_against_local_server() -> PeerCaps {
    negotiate_against_service(lesavka_server::handshake::HandshakeSvc).await
}

fn assert_default_caps(caps: &PeerCaps) {
    assert!(!caps.camera);
    assert!(!caps.microphone);
    assert_eq!(caps.server_version, None);
    assert_eq!(caps.camera_output, None);
    assert_eq!(caps.camera_codec, None);
    assert_eq!(caps.camera_width, None);
    assert_eq!(caps.camera_height, None);
    assert_eq!(caps.camera_fps, None);
    assert_eq!(caps.eye_width, None);
    assert_eq!(caps.eye_height, None);
    assert_eq!(caps.eye_fps, None);
}

fn assert_default_probe(probe_result: &HandshakeProbe) {
    assert_default_caps(&probe_result.caps);
    assert_eq!(probe_result.rtt_ms, None);
    assert!(!probe_result.reachable);
}

struct UnimplementedHandshakeSvc;

#[tonic::async_trait]
impl Handshake for UnimplementedHandshakeSvc {
    async fn get_capabilities(
        &self,
        _req: Request<Empty>,
    ) -> Result<Response<HandshakeSet>, Status> {
        Err(Status::unimplemented("handshake disabled"))
    }
}

struct InternalErrorHandshakeSvc;

#[tonic::async_trait]
impl Handshake for InternalErrorHandshakeSvc {
    async fn get_capabilities(
        &self,
        _req: Request<Empty>,
    ) -> Result<Response<HandshakeSet>, Status> {
        Err(Status::internal("boom"))
    }
}

struct SparseHandshakeSvc;

#[tonic::async_trait]
impl Handshake for SparseHandshakeSvc {
    async fn get_capabilities(
        &self,
        _req: Request<Empty>,
    ) -> Result<Response<HandshakeSet>, Status> {
        Ok(Response::new(HandshakeSet {
            camera: true,
            microphone: false,
            server_version: String::new(),
            camera_output: String::new(),
            camera_codec: String::new(),
            camera_width: 0,
            camera_height: 0,
            camera_fps: 0,
            eye_width: 0,
            eye_height: 0,
            eye_fps: 0,
            server_revision: String::new(),
            bundled_webcam_media: false,
        }))
    }
}

struct SlowHandshakeSvc;

#[tonic::async_trait]
impl Handshake for SlowHandshakeSvc {
    async fn get_capabilities(
        &self,
        _req: Request<Empty>,
    ) -> Result<Response<HandshakeSet>, Status> {
        tokio::time::sleep(Duration::from_secs(6)).await;
        Ok(Response::new(HandshakeSet::default()))
    }
}

#[test]
#[serial]
fn handshake_returns_uvc_caps_with_explicit_dimensions_and_fps() {
    with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
        with_var("LESAVKA_UVC_WIDTH", Some("1024"), || {
            with_var("LESAVKA_UVC_HEIGHT", Some("576"), || {
                with_var("LESAVKA_UVC_FPS", Some("12"), || {
                    let rt = Runtime::new().expect("create runtime");
                    let caps = rt.block_on(negotiate_against_local_server());
                    assert!(caps.camera);
                    assert!(caps.microphone);
                    assert_eq!(
                        caps.server_version,
                        Some(lesavka_server::VERSION.to_string())
                    );
                    assert_eq!(caps.camera_output, Some(String::from("uvc")));
                    assert_eq!(caps.camera_codec, Some(String::from("hevc")));
                    assert_eq!(caps.camera_width, Some(1024));
                    assert_eq!(caps.camera_height, Some(576));
                    assert_eq!(caps.camera_fps, Some(12));
                });
            });
        });
    });
}

#[test]
#[serial]
fn handshake_uses_uvc_interval_when_fps_is_unset() {
    with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
        with_var("LESAVKA_UVC_WIDTH", Some("640"), || {
            with_var("LESAVKA_UVC_HEIGHT", Some("360"), || {
                with_var("LESAVKA_UVC_INTERVAL", Some("250000"), || {
                    let rt = Runtime::new().expect("create runtime");
                    let caps = rt.block_on(negotiate_against_local_server());
                    assert_eq!(caps.camera_output, Some(String::from("uvc")));
                    assert_eq!(caps.camera_codec, Some(String::from("hevc")));
                    assert_eq!(caps.camera_width, Some(640));
                    assert_eq!(caps.camera_height, Some(360));
                    assert_eq!(caps.camera_fps, Some(40));
                    assert!(caps.camera);
                    assert!(caps.microphone);
                });
            });
        });
    });
}

#[test]
#[serial]
fn handshake_returns_hdmi_caps_with_mjpeg_when_h264_decode_is_unavailable() {
    with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || {
        with_var("LESAVKA_DISABLE_UAC", Some("1"), || {
            with_var("LESAVKA_HW_H264", None::<&str>, || {
                with_var("LESAVKA_CAM_CODEC", None::<&str>, || {
                    let _ = lesavka_server::camera::update_camera_config();
                    let rt = Runtime::new().expect("create runtime");
                    let caps = rt.block_on(negotiate_against_local_server());
                    assert_eq!(caps.camera_output, Some(String::from("hdmi")));
                    assert_eq!(caps.camera_codec, Some(String::from("mjpeg")));
                    assert_eq!(caps.camera_fps, Some(30));
                    assert!(!caps.microphone);
                    assert!(caps.camera);
                    assert_eq!(caps.camera_width, Some(1280));
                    assert_eq!(caps.camera_height, Some(720));
                });
            });
        });
    });
}

#[test]
#[serial]
fn handshake_honors_explicit_hdmi_h264_override() {
    with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || {
        with_var("LESAVKA_DISABLE_UAC", Some("1"), || {
            with_var("LESAVKA_HW_H264", None::<&str>, || {
                with_var("LESAVKA_CAM_CODEC", Some("h264"), || {
                    let _ = lesavka_server::camera::update_camera_config();
                    let rt = Runtime::new().expect("create runtime");
                    let caps = rt.block_on(negotiate_against_local_server());
                    assert_eq!(caps.camera_output, Some(String::from("hdmi")));
                    assert_eq!(caps.camera_codec, Some(String::from("h264")));
                    assert_eq!(caps.camera_fps, Some(30));
                    assert!(!caps.microphone);
                    assert!(caps.camera);
                    assert_eq!(caps.camera_width, Some(1280));
                    assert_eq!(caps.camera_height, Some(720));
                });
            });
        });
    });
}

#[test]
#[serial]
fn handshake_auto_mode_falls_back_to_a_valid_camera_configuration() {
    with_var("LESAVKA_CAM_OUTPUT", Some("auto"), || {
        let rt = Runtime::new().expect("create runtime");
        let caps = rt.block_on(negotiate_against_local_server());
        match caps.camera_output.as_deref() {
            Some("uvc") => {
                assert_eq!(caps.camera_codec.as_deref(), Some("hevc"));
                assert!(matches!(caps.camera_width, Some(1280) | Some(640)));
                assert!(matches!(caps.camera_height, Some(720) | Some(360)));
            }
            Some("hdmi") => {
                assert!(matches!(
                    caps.camera_codec.as_deref(),
                    Some("mjpeg") | Some("h264")
                ));
                assert!(matches!(caps.camera_width, Some(1280) | Some(1920)));
                assert!(matches!(caps.camera_height, Some(720) | Some(1080)));
            }
            other => panic!("unexpected camera output: {other:?}"),
        }
        assert!(caps.camera);
    });
}

#[test]
#[serial]
fn handshake_returns_defaults_when_server_is_unreachable() {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
    let addr = listener.local_addr().expect("listener addr");
    drop(listener);

    let rt = Runtime::new().expect("create runtime");
    let caps = rt.block_on(negotiate(&format!("http://{addr}")));
    assert_default_caps(&caps);
}

#[test]
#[serial]
fn handshake_returns_defaults_when_service_is_unimplemented() {
    let rt = Runtime::new().expect("create runtime");
    let caps = rt.block_on(negotiate_against_service(UnimplementedHandshakeSvc));
    assert_default_caps(&caps);
}

#[test]
#[serial]
fn handshake_returns_defaults_when_service_returns_internal_error() {
    let rt = Runtime::new().expect("create runtime");
    let caps = rt.block_on(negotiate_against_service(InternalErrorHandshakeSvc));
    assert_default_caps(&caps);
}

#[test]
#[serial]
fn handshake_maps_empty_optional_fields_to_none() {
    let rt = Runtime::new().expect("create runtime");
    let caps = rt.block_on(negotiate_against_service(SparseHandshakeSvc));
    assert!(caps.camera);
    assert!(!caps.microphone);
    assert_eq!(caps.camera_output, None);
    assert_eq!(caps.camera_codec, None);
    assert_eq!(caps.camera_width, None);
    assert_eq!(caps.camera_height, None);
    assert_eq!(caps.camera_fps, None);
    assert_eq!(caps.eye_width, None);
    assert_eq!(caps.eye_height, None);
    assert_eq!(caps.eye_fps, None);
}

#[test]
#[serial]
fn handshake_times_out_slow_capabilities_call() {
    let rt = Runtime::new().expect("create runtime");
    let caps = rt.block_on(negotiate_against_service(SlowHandshakeSvc));
    assert_default_caps(&caps);
}

#[test]
#[serial]
fn handshake_probe_reports_reachable_caps_and_rtt() {
    with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
        let rt = Runtime::new().expect("create runtime");
        let probe_result = rt.block_on(probe_against_service(
            lesavka_server::handshake::HandshakeSvc,
        ));
        assert!(probe_result.reachable);
        assert!(probe_result.rtt_ms.is_some_and(|rtt| rtt >= 0.0));
        assert!(probe_result.caps.camera);
        assert!(probe_result.caps.microphone);
        assert_eq!(probe_result.caps.camera_output.as_deref(), Some("uvc"));
        assert_eq!(probe_result.caps.camera_codec.as_deref(), Some("hevc"));
    });
}

#[test]
#[serial]
fn handshake_probe_marks_unimplemented_service_reachable() {
    let rt = Runtime::new().expect("create runtime");
    let probe_result = rt.block_on(probe_against_service(UnimplementedHandshakeSvc));
    assert!(probe_result.reachable);
    assert!(probe_result.rtt_ms.is_some());
    assert_default_caps(&probe_result.caps);
}

#[test]
#[serial]
fn handshake_probe_marks_internal_error_service_reachable() {
    let rt = Runtime::new().expect("create runtime");
    let probe_result = rt.block_on(probe_against_service(InternalErrorHandshakeSvc));
    assert!(probe_result.reachable);
    assert!(probe_result.rtt_ms.is_some());
    assert_default_caps(&probe_result.caps);
}

#[test]
#[serial]
fn handshake_probe_maps_empty_optional_fields_to_none() {
    let rt = Runtime::new().expect("create runtime");
    let probe_result = rt.block_on(probe_against_service(SparseHandshakeSvc));
    assert!(probe_result.reachable);
    assert!(probe_result.caps.camera);
    assert!(!probe_result.caps.microphone);
    assert_eq!(probe_result.caps.camera_output, None);
    assert_eq!(probe_result.caps.camera_codec, None);
    assert_eq!(probe_result.caps.camera_width, None);
    assert_eq!(probe_result.caps.camera_height, None);
    assert_eq!(probe_result.caps.camera_fps, None);
}

#[test]
#[serial]
fn handshake_probe_returns_defaults_when_server_is_unreachable() {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
    let addr = listener.local_addr().expect("listener addr");
    drop(listener);

    let rt = Runtime::new().expect("create runtime");
    let probe_result = rt.block_on(probe(&format!("http://{addr}")));
    assert_default_probe(&probe_result);
}

#[test]
#[serial]
fn handshake_probe_returns_defaults_for_invalid_endpoint() {
    let rt = Runtime::new().expect("create runtime");
    let probe_result = rt.block_on(probe("not a uri"));
    assert_default_probe(&probe_result);
}

#[test]
#[serial]
fn handshake_probe_times_out_slow_capabilities_call() {
    let rt = Runtime::new().expect("create runtime");
    let probe_result = rt.block_on(probe_against_service(SlowHandshakeSvc));
    assert_default_probe(&probe_result);
}

#[test]
#[serial]
fn handshake_service_direct_call_reports_capabilities() {
    with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
        let rt = Runtime::new().expect("create runtime");
        let response = rt.block_on(async {
            lesavka_server::handshake::HandshakeSvc
                .get_capabilities(Request::new(Empty {}))
                .await
                .expect("handshake service response")
                .into_inner()
        });
        assert!(response.camera);
        assert!(response.microphone);
        assert_eq!(response.camera_output, "uvc");
        assert_eq!(response.camera_codec, "hevc");
        assert!(response.camera_width > 0);
        assert!(response.camera_height > 0);
        assert!(response.camera_fps > 0);
        assert!(response.eye_width > 0);
        assert!(response.eye_height > 0);
        assert!(response.eye_fps > 0);

        let _ = lesavka_server::handshake::HandshakeSvc::server();
    });
}
