mendi 0.0.2

Rust client for the Mendi neurofeedback headband over BLE using btleplug
Documentation
#![cfg(feature = "simulate")]

use mendi::simulate::{MockDevice, SimConfig, SimulatedDevice};
use mendi::types::*;

// ═══════════════════════════════════════════════════════════════════════════════
//  MockDevice tests
// ═══════════════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn mock_send_receive() {
    let (mut rx, mock) = MockDevice::new(16);
    mock.send(MendiEvent::Connected(DeviceInfo::default()));
    mock.send(MendiEvent::Disconnected);
    drop(mock);

    let e1 = rx.recv().await.unwrap();
    assert!(matches!(e1, MendiEvent::Connected(_)));
    let e2 = rx.recv().await.unwrap();
    assert_eq!(e2, MendiEvent::Disconnected);
    // Channel closed after drop
    assert!(rx.recv().await.is_none());
}

#[tokio::test]
async fn mock_connected_helper() {
    let info = DeviceInfo {
        name: "TestBand".into(),
        id: "TEST-01".into(),
        firmware_version: Some("1.0".into()),
        ..Default::default()
    };
    let (mut rx, mock) = MockDevice::connected(info.clone(), 16);

    let ev = rx.recv().await.unwrap();
    assert_eq!(ev, MendiEvent::Connected(info));

    let ev = rx.recv().await.unwrap();
    assert!(matches!(ev, MendiEvent::Diagnostics(_)));
    if let MendiEvent::Diagnostics(d) = ev {
        assert!(d.imu_ok);
        assert!(d.sensor_ok);
        assert!(d.adc.is_some());
    }

    drop(mock);
    assert!(rx.recv().await.is_none());
}

#[tokio::test]
async fn mock_with_frames() {
    let n = 10;
    let (mut rx, _mock) = MockDevice::with_frames(n, 32);

    // First: Connected
    let ev = rx.recv().await.unwrap();
    assert!(matches!(ev, MendiEvent::Connected(_)));

    // Then: n frames
    let mut frames = vec![];
    for _ in 0..n {
        let ev = rx.recv().await.unwrap();
        if let MendiEvent::Frame(f) = ev {
            frames.push(f);
        } else {
            panic!("expected Frame, got {:?}", ev);
        }
    }

    // Verify frame data is sequential
    assert_eq!(frames.len(), n);
    assert_eq!(frames[0].ir_left, 50_000);
    assert_eq!(frames[1].ir_left, 50_010);
    assert_eq!(frames[9].ir_left, 50_090);
    // Timestamps are sequential at 25Hz (40ms apart)
    assert!((frames[0].timestamp - 0.0).abs() < 0.001);
    assert!((frames[1].timestamp - 40.0).abs() < 0.001);
    // Accel Z ≈ 1g
    assert_eq!(frames[0].acc_z, 16384);
    // Temperature
    assert!((frames[0].temperature - 36.5).abs() < 0.01);

    // Last: Disconnected
    let ev = rx.recv().await.unwrap();
    assert_eq!(ev, MendiEvent::Disconnected);
}

#[tokio::test]
async fn mock_with_frames_zero() {
    let (mut rx, _mock) = MockDevice::with_frames(0, 8);
    let ev = rx.recv().await.unwrap();
    assert!(matches!(ev, MendiEvent::Connected(_)));
    let ev = rx.recv().await.unwrap();
    assert_eq!(ev, MendiEvent::Disconnected);
}

#[tokio::test]
async fn mock_try_send_on_full_channel() {
    let (mut rx, mock) = MockDevice::new(1);
    // First send fills the buffer
    mock.send(MendiEvent::Disconnected);
    // Second send should fail (buffer size 1)
    assert!(!mock.try_send(MendiEvent::Disconnected));
    // Drain
    let _ = rx.recv().await;
}

#[tokio::test]
async fn mock_send_async() {
    let (mut rx, mock) = MockDevice::new(4);
    mock.send_async(MendiEvent::Disconnected).await;
    let ev = rx.recv().await.unwrap();
    assert_eq!(ev, MendiEvent::Disconnected);
}

// ═══════════════════════════════════════════════════════════════════════════════
//  SimulatedDevice tests
// ═══════════════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn sim_emits_connected_then_diagnostics() {
    let config = SimConfig {
        disconnect_after_frames: Some(1),
        ..Default::default()
    };
    let (mut rx, _handle) = SimulatedDevice::start(config);

    let ev = rx.recv().await.unwrap();
    if let MendiEvent::Connected(info) = ev {
        assert_eq!(info.name, "Mendi-SIM");
        assert_eq!(info.firmware_version, Some("SIM-1.0.0".into()));
    } else {
        panic!("expected Connected, got {:?}", ev);
    }

    let ev = rx.recv().await.unwrap();
    assert!(matches!(ev, MendiEvent::Diagnostics(_)));
}

#[tokio::test]
async fn sim_disconnect_after_frames() {
    let config = SimConfig {
        disconnect_after_frames: Some(5),
        frame_rate_hz: 1000.0, // fast so test completes quickly
        ..Default::default()
    };
    let (mut rx, handle) = SimulatedDevice::start(config);

    let mut events = vec![];
    while let Some(ev) = rx.recv().await {
        events.push(ev);
    }

    // Should have: Connected, Diagnostics, 5 Frames, Disconnected
    assert!(events.len() >= 7, "got {} events", events.len());
    assert!(matches!(events[0], MendiEvent::Connected(_)));
    assert!(matches!(events[1], MendiEvent::Diagnostics(_)));

    let frame_count = events.iter().filter(|e| matches!(e, MendiEvent::Frame(_))).count();
    assert_eq!(frame_count, 5);

    assert_eq!(*events.last().unwrap(), MendiEvent::Disconnected);
    assert!(!handle.is_running());
}

#[tokio::test]
async fn sim_handle_disconnect() {
    let config = SimConfig {
        disconnect_after_frames: None, // runs forever
        frame_rate_hz: 100.0,
        ..Default::default()
    };
    let (mut rx, handle) = SimulatedDevice::start(config);

    // Consume a few events
    let _ = rx.recv().await; // Connected
    let _ = rx.recv().await; // Diagnostics
    let _ = rx.recv().await; // Frame

    // Stop it
    handle.disconnect();
    handle.join().await;

    // Drain remaining events, should end with Disconnected
    let mut found_disconnect = false;
    while let Some(ev) = rx.recv().await {
        if ev == MendiEvent::Disconnected {
            found_disconnect = true;
            break;
        }
    }
    assert!(found_disconnect);
}

#[tokio::test]
async fn sim_frame_data_is_realistic() {
    let config = SimConfig {
        disconnect_after_frames: Some(10),
        frame_rate_hz: 1000.0,
        base_ir: 50_000,
        base_red: 40_000,
        base_ambient: 1_000,
        temperature: 37.0,
        ..Default::default()
    };
    let (mut rx, _handle) = SimulatedDevice::start(config);

    // Skip Connected + Diagnostics
    let _ = rx.recv().await;
    let _ = rx.recv().await;

    // Check frames have reasonable values
    let ev = rx.recv().await.unwrap();
    if let MendiEvent::Frame(f) = ev {
        // IR should be near base ± modulation ± noise
        assert!(f.ir_left > 40_000 && f.ir_left < 60_000, "ir_left={}", f.ir_left);
        assert!(f.red_left > 30_000 && f.red_left < 50_000, "red_left={}", f.red_left);
        assert!(f.amb_left > 500 && f.amb_left < 1_500, "amb_left={}", f.amb_left);
        // Temperature
        assert!((f.temperature - 37.0).abs() < 0.01);
        // Accel Z should be near 1g (16384) with noise
        assert!(f.acc_z > 16000 && f.acc_z < 16800, "acc_z={}", f.acc_z);
        // Timestamp should be recent
        assert!(f.timestamp > 0.0);
    } else {
        panic!("expected Frame, got {:?}", ev);
    }
}

#[tokio::test]
async fn sim_battery_events_periodic() {
    let config = SimConfig {
        disconnect_after_frames: Some(150),
        frame_rate_hz: 10_000.0, // very fast
        battery_every_n_frames: 50,
        battery_voltage_mv: 4000,
        charging: true,
        usb_connected: true,
        ..Default::default()
    };
    let (mut rx, _handle) = SimulatedDevice::start(config);

    let mut battery_count = 0;
    while let Some(ev) = rx.recv().await {
        if let MendiEvent::Battery(b) = ev {
            battery_count += 1;
            assert_eq!(b.voltage_mv, 4000);
            assert!(b.charging);
            assert!(b.usb_connected);
            assert_eq!(b.percentage(), 80);
        }
    }
    // With 150 frames and battery every 50, expect 3 battery events
    assert_eq!(battery_count, 3);
}

#[tokio::test]
async fn sim_calibration_events_periodic() {
    let config = SimConfig {
        disconnect_after_frames: Some(200),
        frame_rate_hz: 10_000.0,
        calibration_every_n_frames: 100,
        ..Default::default()
    };
    let (mut rx, _handle) = SimulatedDevice::start(config);

    let mut cal_count = 0;
    while let Some(ev) = rx.recv().await {
        if let MendiEvent::Calibration(c) = ev {
            cal_count += 1;
            assert!(c.auto_calibration);
            assert!(!c.low_power_mode);
        }
    }
    assert_eq!(cal_count, 2);
}

#[tokio::test]
async fn sim_custom_device_name() {
    let config = SimConfig {
        disconnect_after_frames: Some(0),
        device_name: "MyBand-42".into(),
        firmware_version: "2.5.0".into(),
        ..Default::default()
    };
    let (mut rx, _handle) = SimulatedDevice::start(config);

    let ev = rx.recv().await.unwrap();
    if let MendiEvent::Connected(info) = ev {
        assert_eq!(info.name, "MyBand-42");
        assert_eq!(info.firmware_version, Some("2.5.0".into()));
    }
}