//! Safe Rust wrapper for a small M5Unified C ABI surface.
//!
//! The API is intentionally shaped around M5Unified's common examples while
//! keeping Rust call sites safe and host-checkable. Hardware calls are provided
//! by `m5unified-sys`; on non-ESP-IDF targets that crate supplies no-op stubs so
//! examples compile in CI.
//!
//! # Example
//!
//! ```no_run
//! use m5unified::{colors, M5Unified};
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let mut m5 = M5Unified::begin()?;
//!
//!     m5.display.fill_screen(colors::BLACK);
//!     m5.display.set_text_size(2);
//!     m5.display.println("hello from Rust")?;
//!
//!     loop {
//!         m5.update();
//!         if m5.buttons.a().was_pressed() {
//!             m5.display.println("Button A")?;
//!         }
//!         m5.delay_ms(16);
//!     }
//! }
//! ```

mod audio;
mod buttons;
mod canvas;
mod cardputer;
mod config;
mod display;
mod error;
mod i2c;
mod imu;
mod io_expander;
mod led;
mod log;
mod power;
mod rtc;
mod sd;
mod servo;
mod system;
mod timer;
mod touch;

pub use audio::{
    AudioCapture, AudioCaptureConfig, AudioQueueState, Mic, MicConfig, RawPlaybackOptions,
    RecordingOptions, Speaker, SpeakerConfig, ToneOptions, WavPlaybackOptions,
};
pub use buttons::{Button, ButtonId, ButtonState, Buttons};
pub use canvas::Canvas;
pub use cardputer::{
    Cardputer, CardputerGrove, CardputerIr, CardputerKeyValue, CardputerKeyboard,
    CardputerKeyboardState, CardputerSd, CardputerSdDirEntry, CardputerSdPins, CardputerSpi,
    GpioMode, GrovePin, I2cAddress, NecFrame, SdCardInfo, SdCardType, SpiBitOrder, SpiConfig,
    SpiMode, SpiPins,
};
pub use config::{ExternalDisplayConfig, ExternalSpeakerConfig, M5UnifiedConfig};
pub use display::{
    colors, Color565, ColorDepth, Display, DisplayFont, DisplayKind, DisplayRef, EpdMode,
    GradientFillStyle, ImageDrawOptions, ImageFormat, Point, QrCodeOptions, Rect, Size, TextDatum,
};
pub use error::Error;
pub use i2c::{I2cBus, I2cDevice};
pub use imu::{
    Imu, ImuAxis, ImuConvertParams, ImuData, ImuDevice, ImuDeviceKind, ImuKind, ImuRawData,
    ImuSensorMask, RawVec3, Vec3,
};
pub use io_expander::{IoExpander, Pi4ioe5v6408};
pub use led::{
    Led, LedColor, LedPowerHub, LedStrip, LedStripColorOrder, LedStripConfig, LedStripRmtConfig,
    LedType,
};
pub use log::{Log, LogLevel, LogTarget, RawLogCallback};
pub use power::{
    Aw32001, Aw32001ChargeStatus, Axp192, Axp192PekPress, Axp2101, Axp2101ChargeStatus,
    Axp2101IrqStatus, Axp2101PekPress, Bq27220, ChargeState, ExtPortBusConfig, ExtPortMask, Ina226,
    Ina226Config, Ina226ConversionTime, Ina226Mode, Ina226Sampling, Ina3221, Ip5306, Power,
    PowerType, Py32Pmic, Py32PmicPekPress,
};
pub use rtc::{Date, DateTime, Rtc, RtcDevice, RtcDeviceKind, Time};
pub use sd::{
    sd_begin, sd_begin_with_config, sd_end, sd_is_mounted, SdCard, SdSpiConfig, SD_MOUNT_PATH,
};
pub use servo::{StackChanServos, PITCH_ZERO_DEFAULT, SERVO_PITCH, SERVO_YAW, YAW_ZERO_DEFAULT};
pub(crate) use system::raw_display_kinds;
pub use system::{heap_stats, Board, HeapCaps, HeapStats, PinName};
pub use timer::M5Timer;
pub use touch::{Touch, TouchDetail, TouchPoint, TouchState};

/// Top-level handle for M5Unified-backed board features.
#[derive(Debug)]
pub struct M5Unified {
    pub display: Display,
    pub buttons: Buttons,
    pub mic: Mic,
    pub speaker: Speaker,
    pub in_i2c: I2cBus,
    pub ex_i2c: I2cBus,
    pub imu: Imu,
    pub touch: Touch,
    pub rtc: Rtc,
    pub power: Power,
    pub led: Led,
    pub log: Log,
}

impl M5Unified {
    /// Initialize M5Unified and return a board handle.
    pub fn begin() -> Result<Self, Error> {
        let ok = unsafe { m5unified_sys::m5u_begin() };
        Self::from_begin_result(ok)
    }

    pub fn begin_with_config(config: &M5UnifiedConfig) -> Result<Self, Error> {
        let raw = config.to_raw();
        let ok = unsafe { m5unified_sys::m5u_begin_with_config(&raw) };
        Self::from_begin_result(ok)
    }

    fn from_begin_result(ok: bool) -> Result<Self, Error> {
        if !ok {
            return Err(Error::BeginFailed);
        }

        Ok(Self {
            display: Display,
            buttons: Buttons,
            mic: Mic,
            speaker: Speaker,
            in_i2c: I2cBus::INTERNAL,
            ex_i2c: I2cBus::EXTERNAL,
            imu: Imu,
            touch: Touch,
            rtc: Rtc,
            power: Power,
            led: Led,
            log: Log,
        })
    }

    /// Alias for the primary display, matching upstream's `M5.Lcd` naming.
    pub fn lcd(&self) -> &Display {
        &self.display
    }

    /// Mutable alias for the primary display, matching upstream's `M5.Lcd`.
    pub fn lcd_mut(&mut self) -> &mut Display {
        &mut self.display
    }

    pub fn button(&self, index: usize) -> Option<Button> {
        self.buttons.get(index)
    }

    pub fn buttons(&self, index: usize) -> Option<Button> {
        self.button(index)
    }

    pub fn displays(&self, index: usize) -> Option<DisplayRef> {
        self.display(index)
    }

    /// Poll/update M5Unified internals, including button edge state.
    pub fn update(&mut self) {
        unsafe { m5unified_sys::m5u_update() }
    }

    /// Delay execution. On host builds this is currently a no-op.
    pub fn delay_ms(&self, ms: u32) {
        unsafe { m5unified_sys::m5u_delay_ms(ms) }
    }
}

#[cfg(test)]
mod tests {
    use core::fmt::Write as _;

    use super::*;

    #[test]
    fn display_dimensions_are_available_on_host_stubs() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert!(m5.display.width() > 0);
        assert!(m5.display.height() > 0);
        assert_eq!(m5.lcd().width(), m5.display.width());
        assert_eq!(m5.lcd_mut().height(), m5.display.height());
        assert!(m5.button(0).is_some());
        assert!(m5.buttons(0).is_some());
        m5.display.draw_pixel(1, 1, colors::WHITE);
        m5.display.write_pixel(2, 2, colors::RED);
        m5.display.draw_fast_hline(0, 4, 12, colors::GREEN);
        m5.display.write_fast_hline(0, 5, 12, colors::GREEN);
        m5.display.draw_fast_vline(4, 0, 12, colors::BLUE);
        m5.display.write_fast_vline(5, 0, 12, colors::BLUE);
        m5.display.draw_round_rect(8, 8, 24, 16, 4, colors::WHITE);
        m5.display.fill_round_rect(10, 10, 20, 12, 3, colors::RED);
        let p0 = Point { x: 4, y: 4 };
        let p1 = Point { x: 16, y: 4 };
        let p2 = Point { x: 10, y: 18 };
        let p3 = Point { x: 20, y: 18 };
        let line_start = Point { x: 0, y: 0 };
        let line_end = Point { x: 20, y: 10 };
        let image_rect = Rect {
            x: 0,
            y: 0,
            w: 2,
            h: 2,
        };
        let scroll_rect = Rect {
            x: 0,
            y: 0,
            w: 16,
            h: 16,
        };
        assert_eq!(ColorDepth::RGB565_2BYTE.raw(), 16);
        assert_eq!(ColorDepth::RGB565_2BYTE.bits(), 16);
        assert!(!ColorDepth::RGB565_2BYTE.has_palette());
        assert_eq!(Color565::rgb888(255, 0, 0), Color565(colors::RED));
        assert_eq!(m5.display.color888(0x12, 0x34, 0x56), 0x123456);
        let image = [colors::RED, colors::GREEN, colors::BLUE, colors::WHITE];
        let encoded_image = [0xFF_u8];
        let image_options = ImageDrawOptions {
            position: Point { x: 1, y: 2 },
            max_size: Size { w: 32, h: 24 },
            offset: Point { x: 0, y: 0 },
            scale_x: 1.0,
            scale_y: 1.0,
            datum: TextDatum::MiddleCenter,
        };
        m5.display.draw_triangle(p0, p1, p2, colors::YELLOW);
        m5.display.fill_triangle(p0, p1, p2, colors::CYAN);
        m5.display.draw_ellipse(30, 30, 8, 4, colors::WHITE);
        m5.display.fill_ellipse(32, 32, 6, 3, colors::RED);
        m5.display
            .draw_arc(Point { x: 40, y: 40 }, 8, 12, 0.0, 90.0, colors::GREEN);
        m5.display
            .fill_arc(Point { x: 45, y: 45 }, 6, 10, 90.0, 180.0, colors::BLUE);
        m5.display.draw_ellipse_arc(
            Point { x: 50, y: 50 },
            Size { w: 4, h: 6 },
            Size { w: 8, h: 12 },
            0.0,
            180.0,
            colors::WHITE,
        );
        m5.display.fill_ellipse_arc(
            Point { x: 55, y: 55 },
            Size { w: 3, h: 5 },
            Size { w: 7, h: 11 },
            180.0,
            270.0,
            colors::RED,
        );
        m5.display.draw_quadratic_bezier(p0, p1, p2, colors::WHITE);
        m5.display.draw_cubic_bezier(p0, p1, p2, p3, colors::WHITE);
        m5.display
            .draw_smooth_line(line_start, line_end, colors::CYAN);
        m5.display
            .draw_wide_line(line_start, line_end, 1.5, colors::CYAN);
        m5.display
            .draw_wedge_line(line_start, line_end, 0.5, 2.0, colors::CYAN);
        m5.display
            .draw_gradient_line(line_start, line_end, colors::RED, colors::BLUE);
        m5.display
            .draw_spot(Point { x: 12, y: 12 }, 2.5, colors::WHITE);
        m5.display
            .fill_smooth_circle(Point { x: 14, y: 14 }, 4, colors::GREEN);
        m5.display
            .fill_smooth_round_rect(image_rect, 2, colors::BLUE);
        m5.display.fill_gradient_rect(
            scroll_rect,
            colors::RED,
            colors::BLUE,
            GradientFillStyle::HorizontalLinear,
        );
        m5.display.fill_gradient_rect(
            scroll_rect,
            colors::RED,
            colors::BLUE,
            GradientFillStyle::RadialCenter,
        );
        m5.display.flood_fill(Point { x: 0, y: 0 }, colors::BLACK);
        m5.display.paint(Point { x: 1, y: 1 }, colors::BLACK);
        m5.display.set_scroll_rect(scroll_rect);
        m5.display.set_scroll_rect_color(scroll_rect, colors::BLACK);
        assert_eq!(m5.display.scroll_rect(), Rect::default());
        m5.display.clear_scroll_rect();
        m5.display.scroll(1, -1);
        assert_eq!(m5.display.cursor_x(), 0);
        m5.display.set_brightness(64);
        assert_eq!(m5.display.brightness(), 0);
        m5.display.sleep();
        m5.display.wakeup();
        m5.display.power_save(true);
        m5.display.power_save(false);
        m5.display.invert_display(true);
        assert!(!m5.display.inverted());
        m5.display.set_swap_bytes(true);
        assert!(!m5.display.swap_bytes());
        m5.display.set_color_depth(ColorDepth::RGB565_2BYTE);
        assert_eq!(m5.display.color_depth(), ColorDepth::RGB565_2BYTE);
        m5.display.set_addr_window(scroll_rect);
        m5.display
            .set_window(Point { x: 0, y: 0 }, Point { x: 1, y: 1 });
        m5.display.begin_transaction();
        assert_eq!(m5.display.start_count(), 0);
        m5.display.end_transaction();
        assert_eq!(m5.display.scan_line(), 0);
        m5.display.set_raw_color(0x123456);
        assert_eq!(m5.display.raw_color(), 0);
        m5.display.set_base_color(0x123456);
        assert_eq!(m5.display.base_color(), 0);
        m5.display.write_color(colors::WHITE, 2);
        m5.display.draw_pixel_current(0, 0);
        m5.display.write_pixel_current(0, 0);
        m5.display.write_fill_rect(image_rect, colors::WHITE);
        m5.display
            .write_fill_rect_preclipped(image_rect, colors::WHITE);
        m5.display.push_block(colors::BLACK, 2);
        m5.display.progress_bar(scroll_rect, 50);
        m5.display.push_state();
        m5.display.pop_state();
        m5.display.display();
        m5.display.display_region(scroll_rect);
        assert!(!m5.display.display_busy());
        m5.display.wait_display();
        assert!(!m5.display.has_palette());
        assert_eq!(m5.display.palette_count(), 0);
        assert!(!m5.display.is_readable());
        assert!(!m5.display.is_epd());
        assert!(!m5.display.is_bus_shared());
        m5.display.set_auto_display(true);
        m5.display.init_dma();
        m5.display.wait_dma();
        assert!(!m5.display.dma_busy());
        assert_eq!(m5.display.font_width(), 6);
        assert_eq!(m5.display.draw_center_string("center", 20, 20), Ok(0));
        assert_eq!(m5.display.draw_right_string("right", 20, 30), Ok(0));
        assert_eq!(m5.display.draw_number(1234, 0, 0), 0);
        assert_eq!(m5.display.draw_float(12.5, 1, 0, 0), 0);
        assert_eq!(m5.display.draw_char(b'A' as u16, 0, 0), 0);
        assert_eq!(m5.display.text_width("host").unwrap(), 0);
        assert_eq!(m5.display.text_length("host", 120).unwrap(), 0);
        assert_eq!(
            m5.display
                .qr_code("https://m5stack.com", QrCodeOptions::default()),
            Ok(())
        );
        assert_eq!(m5.display.text_datum(), Some(TextDatum::TopLeft));
        m5.display.set_text_padding(12);
        assert_eq!(m5.display.text_padding(), 0);
        assert_eq!(m5.display.text_size_x(), 1.0);
        assert_eq!(m5.display.text_size_y(), 1.0);
        m5.display.set_clip_rect(scroll_rect);
        assert_eq!(m5.display.clip_rect(), Rect::default());
        m5.display.clear_clip_rect();
        m5.display.set_pivot(1.0, 2.0);
        assert_eq!(m5.display.pivot(), (0.0, 0.0));
        assert_eq!(m5.display.push_image_rgb565(image_rect, &image), Ok(()));
        assert_eq!(
            m5.display
                .push_image_rgb565_transparent(image_rect, &image, colors::BLACK),
            Ok(())
        );
        assert_eq!(
            m5.display.push_image_rgb565(image_rect, &image[..3]),
            Err(Error::InvalidBufferLength)
        );
        assert_eq!(m5.display.read_pixel(0, 0), 0);
        let mut pixels = [colors::WHITE; 4];
        assert_eq!(m5.display.read_rect_rgb565(image_rect, &mut pixels), Ok(()));
        assert_eq!(pixels, [0_u16; 4]);
        m5.display.copy_rect(
            Point { x: 4, y: 4 },
            Size { w: 2, h: 2 },
            Point { x: 0, y: 0 },
        );
        assert_eq!(
            m5.display
                .draw_image(ImageFormat::Bmp, &encoded_image, image_options),
            Ok(())
        );
        assert_eq!(m5.display.draw_bmp(&encoded_image, image_options), Ok(()));
        assert_eq!(m5.display.draw_jpg(&encoded_image, image_options), Ok(()));
        assert_eq!(m5.display.draw_png(&encoded_image, image_options), Ok(()));
        assert_eq!(m5.display.draw_qoi(&encoded_image, image_options), Ok(()));
        assert_eq!(
            m5.display.draw_bmp(&[], ImageDrawOptions::default()),
            Err(Error::InvalidBufferLength)
        );
        assert_eq!(
            m5.display
                .draw_bmp_file("/tmp/missing.bmp", ImageDrawOptions::default()),
            Ok(())
        );
        assert_eq!(
            m5.display
                .draw_jpg_file("/tmp/missing.jpg", ImageDrawOptions::default()),
            Ok(())
        );
        assert_eq!(
            m5.display
                .draw_png_file("/tmp/missing.png", ImageDrawOptions::default()),
            Ok(())
        );
        assert_eq!(
            m5.display
                .draw_qoi_file("/tmp/missing.qoi", ImageDrawOptions::default()),
            Ok(())
        );
        let mut display = m5.displays(0).expect("host stub display should exist");
        display.draw_pixel(1, 1, colors::WHITE);
        display.write_pixel(2, 2, colors::RED);
        display.draw_fast_hline(0, 4, 12, colors::GREEN);
        display.write_fast_hline(0, 5, 12, colors::GREEN);
        display.draw_fast_vline(4, 0, 12, colors::BLUE);
        display.write_fast_vline(5, 0, 12, colors::BLUE);
        display.draw_round_rect(8, 8, 24, 16, 4, colors::WHITE);
        display.fill_round_rect(10, 10, 20, 12, 3, colors::RED);
        display.draw_triangle(p0, p1, p2, colors::YELLOW);
        display.fill_triangle(p0, p1, p2, colors::CYAN);
        display.draw_ellipse(30, 30, 8, 4, colors::WHITE);
        display.fill_ellipse(32, 32, 6, 3, colors::RED);
        display.draw_arc(Point { x: 40, y: 40 }, 8, 12, 0.0, 90.0, colors::GREEN);
        display.fill_arc(Point { x: 45, y: 45 }, 6, 10, 90.0, 180.0, colors::BLUE);
        display.draw_ellipse_arc(
            Point { x: 50, y: 50 },
            Size { w: 4, h: 6 },
            Size { w: 8, h: 12 },
            0.0,
            180.0,
            colors::WHITE,
        );
        display.fill_ellipse_arc(
            Point { x: 55, y: 55 },
            Size { w: 3, h: 5 },
            Size { w: 7, h: 11 },
            180.0,
            270.0,
            colors::RED,
        );
        display.draw_quadratic_bezier(p0, p1, p2, colors::WHITE);
        display.draw_cubic_bezier(p0, p1, p2, p3, colors::WHITE);
        display.draw_smooth_line(line_start, line_end, colors::CYAN);
        display.draw_wide_line(line_start, line_end, 1.5, colors::CYAN);
        display.draw_wedge_line(line_start, line_end, 0.5, 2.0, colors::CYAN);
        display.draw_gradient_line(line_start, line_end, colors::RED, colors::BLUE);
        display.draw_spot(Point { x: 12, y: 12 }, 2.5, colors::WHITE);
        display.fill_smooth_circle(Point { x: 14, y: 14 }, 4, colors::GREEN);
        display.fill_smooth_round_rect(image_rect, 2, colors::BLUE);
        display.fill_gradient_rect(
            scroll_rect,
            colors::RED,
            colors::BLUE,
            GradientFillStyle::VerticalLinear,
        );
        display.flood_fill(Point { x: 0, y: 0 }, colors::BLACK);
        display.paint(Point { x: 1, y: 1 }, colors::BLACK);
        display.set_scroll_rect(scroll_rect);
        display.set_scroll_rect_color(scroll_rect, colors::BLACK);
        assert_eq!(display.scroll_rect(), Rect::default());
        display.clear_scroll_rect();
        display.scroll(1, -1);
        assert_eq!(display.cursor_x(), 0);
        assert_eq!(display.cursor_y(), 0);
        display.set_brightness(64);
        assert_eq!(display.brightness(), 0);
        display.sleep();
        display.wakeup();
        display.power_save(true);
        display.power_save(false);
        display.invert_display(true);
        assert!(!display.inverted());
        display.set_swap_bytes(true);
        assert!(!display.swap_bytes());
        display.set_color_depth(ColorDepth::RGB565_2BYTE);
        assert_eq!(display.color_depth(), ColorDepth::RGB565_2BYTE);
        display.set_addr_window(scroll_rect);
        display.set_window(Point { x: 0, y: 0 }, Point { x: 1, y: 1 });
        display.begin_transaction();
        assert_eq!(display.start_count(), 0);
        display.end_transaction();
        assert_eq!(display.scan_line(), 0);
        display.set_raw_color(0x123456);
        assert_eq!(display.raw_color(), 0);
        display.set_base_color(0x123456);
        assert_eq!(display.base_color(), 0);
        display.write_color(colors::WHITE, 2);
        display.draw_pixel_current(0, 0);
        display.write_pixel_current(0, 0);
        display.write_fill_rect(image_rect, colors::WHITE);
        display.write_fill_rect_preclipped(image_rect, colors::WHITE);
        display.push_block(colors::BLACK, 2);
        display.progress_bar(scroll_rect, 50);
        display.push_state();
        display.pop_state();
        display.display();
        display.display_region(scroll_rect);
        assert!(!display.display_busy());
        display.wait_display();
        assert!(!display.has_palette());
        assert_eq!(display.palette_count(), 0);
        assert!(!display.is_readable());
        assert!(!display.is_epd());
        assert!(!display.is_bus_shared());
        display.set_auto_display(true);
        display.init_dma();
        display.wait_dma();
        assert!(!display.dma_busy());
        assert_eq!(display.font_width(), 6);
        assert_eq!(display.draw_center_string("center", 20, 20), Ok(0));
        assert_eq!(display.draw_right_string("right", 20, 30), Ok(0));
        assert_eq!(display.draw_number(1234, 0, 0), 0);
        assert_eq!(display.draw_float(12.5, 1, 0, 0), 0);
        assert_eq!(display.draw_char(b'A' as u16, 0, 0), 0);
        assert_eq!(display.text_width("host").unwrap(), 0);
        assert_eq!(display.text_length("host", 120).unwrap(), 0);
        assert_eq!(
            display.qr_code("https://m5stack.com", QrCodeOptions::default()),
            Ok(())
        );
        assert_eq!(display.text_datum(), Some(TextDatum::TopLeft));
        display.set_text_padding(12);
        assert_eq!(display.text_padding(), 0);
        assert_eq!(display.text_size_x(), 1.0);
        assert_eq!(display.text_size_y(), 1.0);
        display.set_clip_rect(scroll_rect);
        assert_eq!(display.clip_rect(), Rect::default());
        display.clear_clip_rect();
        display.set_pivot(1.0, 2.0);
        assert_eq!(display.pivot(), (0.0, 0.0));
        assert_eq!(display.push_image_rgb565(image_rect, &image), Ok(()));
        assert_eq!(
            display.push_image_rgb565_transparent(image_rect, &image, colors::BLACK),
            Ok(())
        );
        assert_eq!(
            display.push_image_rgb565(image_rect, &image[..3]),
            Err(Error::InvalidBufferLength)
        );
        assert_eq!(display.read_pixel(0, 0), 0);
        let mut pixels = [colors::WHITE; 4];
        assert_eq!(display.read_rect_rgb565(image_rect, &mut pixels), Ok(()));
        assert_eq!(pixels, [0_u16; 4]);
        display.copy_rect(
            Point { x: 4, y: 4 },
            Size { w: 2, h: 2 },
            Point { x: 0, y: 0 },
        );
        assert_eq!(
            display.draw_image(ImageFormat::Bmp, &encoded_image, image_options),
            Ok(())
        );
        assert_eq!(display.draw_bmp(&encoded_image, image_options), Ok(()));
        assert_eq!(display.draw_jpg(&encoded_image, image_options), Ok(()));
        assert_eq!(display.draw_png(&encoded_image, image_options), Ok(()));
        assert_eq!(display.draw_qoi(&encoded_image, image_options), Ok(()));
        assert_eq!(
            display.draw_bmp(&[], ImageDrawOptions::default()),
            Err(Error::InvalidBufferLength)
        );
        assert_eq!(
            display.draw_bmp_file("/tmp/missing.bmp", ImageDrawOptions::default()),
            Ok(())
        );
        assert_eq!(
            display.draw_jpg_file("/tmp/missing.jpg", ImageDrawOptions::default()),
            Ok(())
        );
        assert_eq!(
            display.draw_png_file("/tmp/missing.png", ImageDrawOptions::default()),
            Ok(())
        );
        assert_eq!(
            display.draw_qoi_file("/tmp/missing.qoi", ImageDrawOptions::default()),
            Ok(())
        );
    }

    #[test]
    fn invalid_strings_are_rejected_before_ffi() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert_eq!(m5.display.print("bad\0string"), Err(Error::InvalidString));
        assert_eq!(
            m5.display.text_width("bad\0string"),
            Err(Error::InvalidString)
        );
        assert_eq!(
            m5.display.draw_center_string("bad\0string", 0, 0),
            Err(Error::InvalidString)
        );
        assert_eq!(
            m5.display.draw_right_string("bad\0string", 0, 0),
            Err(Error::InvalidString)
        );
        assert_eq!(
            m5.display.text_length("bad\0string", 80),
            Err(Error::InvalidString)
        );
        assert_eq!(
            m5.display.qr_code("bad\0string", QrCodeOptions::default()),
            Err(Error::InvalidString)
        );
        assert_eq!(
            m5.display
                .draw_bmp_file("bad\0path", ImageDrawOptions::default()),
            Err(Error::InvalidString)
        );
        let mut display = m5.displays(0).expect("host stub display should exist");
        assert_eq!(display.text_width("bad\0string"), Err(Error::InvalidString));
        assert_eq!(
            display.draw_center_string("bad\0string", 0, 0),
            Err(Error::InvalidString)
        );
        assert_eq!(
            display.draw_right_string("bad\0string", 0, 0),
            Err(Error::InvalidString)
        );
        assert_eq!(
            display.text_length("bad\0string", 80),
            Err(Error::InvalidString)
        );
        assert_eq!(
            display.qr_code("bad\0string", QrCodeOptions::default()),
            Err(Error::InvalidString)
        );
        assert_eq!(
            display.draw_bmp_file("bad\0path", ImageDrawOptions::default()),
            Err(Error::InvalidString)
        );
    }

    #[test]
    fn mic_rms_uses_recorded_buffer() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        let mut buffer = [0_i16; 8];
        assert_eq!(m5.mic.rms(&mut buffer), Some(0.0));
    }

    #[test]
    fn audio_config_helpers_compile_on_host() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");

        let mut mic = m5.mic.config();
        assert_eq!(mic.sample_rate, 16_000);
        mic.dma_buf_count = 3;
        mic.dma_buf_len = 256;
        mic.noise_filter_level = 8;
        assert_eq!(m5.mic.set_config(mic), Ok(()));
        m5.mic.set_sample_rate(24_000);
        assert_eq!(m5.mic.noise_filter_level(), 0);
        assert!(m5.mic.set_noise_filter_level(8));
        assert!(!m5.mic.is_running());
        assert_eq!(m5.mic.recording_state(), AudioQueueState::Idle);
        let mut rec_u8 = [0_u8; 8];
        assert!(m5.mic.record_u8(&mut rec_u8));
        assert!(m5.mic.record_u8_at(&mut rec_u8, 24_000));
        assert!(m5.mic.record_i16_with_options(
            &mut [0_i16; 8],
            RecordingOptions {
                sample_rate_hz: 24_000,
                stereo: true,
            }
        ));

        let mut speaker = m5.speaker.config();
        assert_eq!(speaker.sample_rate, 48_000);
        speaker.sample_rate = 96_000;
        speaker.dma_buf_count = 20;
        assert_eq!(m5.speaker.set_config(speaker), Ok(()));
        assert!(!m5.speaker.is_running());
        assert_eq!(m5.speaker.playing_channels(), 0);
        assert_eq!(m5.speaker.channel_playing_state(0), AudioQueueState::Idle);
        assert!(m5.speaker.tone_with_options(
            440.0,
            ToneOptions {
                duration_ms: 100,
                channel: Some(1),
                stop_current_sound: false,
            }
        ));
        assert!(m5
            .speaker
            .tone_with_raw(440.0, &[0x80; 16], ToneOptions::default(), false));
        assert!(m5.speaker.play_i8_with_options(
            &[0_i8; 8],
            RawPlaybackOptions {
                sample_rate_hz: 22_050,
                stereo: false,
                repeat: 2,
                channel: Some(2),
                stop_current_sound: true,
            }
        ));
        assert!(m5
            .speaker
            .play_i16_with_options(&[0_i16; 8], RawPlaybackOptions::default()));
        assert!(m5
            .speaker
            .play_u8_with_options(&[0_u8; 8], RawPlaybackOptions::default()));
        assert!(m5
            .speaker
            .play_wav_with_options(&[0_u8; 44], WavPlaybackOptions::default()));
    }

    #[test]
    fn imu_combined_data_uses_host_stub() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert_eq!(m5.imu.kind(), ImuKind::None);
        assert!(m5.imu.init());
        assert!(!m5.imu.begin_for_board(Board::M5Stack));
        assert!(!m5.imu.init_for_board(Board::M5Stack));
        assert!(m5.imu.update());
        assert!(m5.imu.update_mask().is_empty());
        let data = m5.imu.data().expect("host stub imu data should exist");
        assert_eq!(data.usec, 0);
        assert_eq!(data.accel.z, 1.0);
        assert_eq!(data.gyro, Vec3::default());
        assert_eq!(data.mag, Vec3::default());
        assert_eq!(data.temperature_c, Some(25.0));
        assert_eq!(m5.imu.accel_data().unwrap().z, 1.0);
        assert_eq!(m5.imu.gyro_data(), Some(Vec3::default()));
        assert_eq!(m5.imu.gyro_mag(), Some(Vec3::default()));
        assert!(m5
            .imu
            .set_axis_order(ImuAxis::XPos, ImuAxis::YPos, ImuAxis::ZPos));
        assert!(m5
            .imu
            .set_axis_order_right_handed(ImuAxis::XPos, ImuAxis::YPos));
        assert!(m5
            .imu
            .set_axis_order_left_handed(ImuAxis::XPos, ImuAxis::YPos));
        assert!(m5.imu.set_int_pin_active_logic(true));
        m5.imu.set_clock_hz(400_000);
        m5.imu.set_calibration_strength(1, 2, 3);
        m5.imu.clear_offset_data();
        m5.imu.set_offset_data(0, 123);
        assert_eq!(m5.imu.offset_data_i32(0), 0);
        assert_eq!(m5.imu.raw_data(0), 0);
        assert!(m5.imu.sleep());

        let mut bmi270 = m5.imu.bmi270();
        assert_eq!(bmi270.kind(), ImuDeviceKind::Bmi270);
        assert!(bmi270.begin().is_empty());
        assert!(bmi270.init().is_empty());
        assert!(bmi270.raw_data().is_none());
        assert!(bmi270.data().is_none());
        assert!(bmi270.temperature_adc().is_none());
        assert!(bmi270.temperature_c().is_none());
        assert!(!bmi270.sleep());
        assert!(!bmi270.set_int_pin_active_logic(true));
        assert_eq!(bmi270.who_am_i(), None);
        let params = bmi270
            .convert_params()
            .expect("host stub convert params should exist");
        assert_eq!(params.temperature_c(25), 25.0);
        assert_eq!(m5.imu.ak8963().kind(), ImuDeviceKind::Ak8963);
        assert_eq!(m5.imu.bmm150().kind(), ImuDeviceKind::Bmm150);
        assert_eq!(m5.imu.mpu6886().kind(), ImuDeviceKind::Mpu6886);
        assert_eq!(m5.imu.sh200q().kind(), ImuDeviceKind::Sh200q);

        let mask = ImuSensorMask::from_raw(ImuSensorMask::ACCEL.raw() | ImuSensorMask::GYRO.raw());
        assert!(mask.contains(ImuSensorMask::ACCEL));
        assert!(mask.contains(ImuSensorMask::GYRO));
        assert!(!mask.contains(ImuSensorMask::MAG));
        let raw_vec = RawVec3::new(2, -4, 8);
        assert_eq!(
            raw_vec.scaled(0.5),
            Vec3 {
                x: 1.0,
                y: -2.0,
                z: 4.0
            }
        );
        assert_eq!(ImuKind::Bmi270.raw(), 6);
    }

    #[test]
    fn led_host_stub_reports_disabled() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert!(!m5.led.is_enabled());
        assert_eq!(m5.led.count(), 0);
        assert_eq!(m5.led.led_type(0), LedType::Unknown);
        m5.led.set_all_color(LedColor::RED);
        m5.led
            .set_colors(0, &[LedColor::RED, LedColor::GREEN, LedColor::BLUE]);
        let mut power_hub = m5.led.power_hub();
        assert!(!power_hub.begin());
        assert_eq!(power_hub.count(), 0);
        assert_eq!(power_hub.led_type(0), LedType::Unknown);
        power_hub.set_brightness(32);
        power_hub.set_color(0, LedColor::WHITE);
        power_hub.set_colors(0, &[LedColor::RED, LedColor::GREEN]);
        power_hub.display();
        let mut strip = m5.led.strip();
        assert_eq!(LedStripColorOrder::from_raw(2), LedStripColorOrder::Grb);
        assert_eq!(LedStripColorOrder::Bgr.raw(), 5);
        assert_eq!(
            LedStripConfig::default(),
            LedStripConfig {
                led_count: 1,
                color_order: LedStripColorOrder::Grb,
                byte_per_led: 3
            }
        );
        assert_eq!(LedStripRmtConfig::new(5).pin_data, 5);
        assert!(!strip.set_config(LedStripConfig::default()));
        assert!(!strip.set_rmt_bus_config(LedStripRmtConfig::new(5)));
        assert!(!strip.begin());
        assert_eq!(strip.count(), 0);
        assert_eq!(strip.led_type(0), LedType::Unknown);
        strip.set_brightness(64);
        strip.set_color(0, LedColor::WHITE);
        strip.set_colors(0, &[LedColor::RED, LedColor::GREEN]);
        strip.display();
    }

    #[test]
    fn system_host_stub_reports_unknown_board_and_no_pins() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert_eq!(m5.board(), Board::Unknown);
        assert_eq!(m5.get_pin(PinName::PORT_A_SDA), None);
        assert!(m5.set_primary_display(0));
        assert!(!m5.set_primary_display_type(DisplayKind::ModuleDisplay));
        assert!(
            !m5.set_primary_display_types(&[DisplayKind::UnitOled, DisplayKind::ModuleDisplay,])
        );
        m5.set_touch_button_height(32);
        m5.set_log_display_types(&[DisplayKind::UnitOled, DisplayKind::ModuleDisplay]);
        assert_eq!(m5.touch_button_height(), 0);
        assert_eq!(m5.millis(), 0);
        assert_eq!(m5.micros(), 0);
        assert_eq!(m5.update_msec(), 0);
        assert_eq!(Board::from_raw(1), Board::M5Stack);
        assert_eq!(Board::from_raw(199), Board::M5ModuleDisplay);
        assert_eq!(Board::from_raw(999), Board::Raw(999));
        assert_eq!(Board::M5AtomS3Lite.raw(), 137);
        assert_eq!(DisplayKind::ModuleDisplay.raw(), 199);
        assert_eq!(
            m5.display_index_any(&[DisplayKind::UnitOled, DisplayKind::ModuleDisplay]),
            None
        );
        assert!(!m5.in_i2c.is_enabled());
        assert_eq!(m5.in_i2c.port(), None);
        assert_eq!(m5.in_i2c.sda_pin(), None);
        assert_eq!(m5.in_i2c.scl_pin(), None);
        let io_expander = m5.io_expander(0);
        assert_eq!(io_expander.index(), 0);
        assert!(!io_expander.available());
        assert!(!io_expander.set_direction(0, true));
        assert!(!io_expander.enable_pull(0, true));
        assert!(!io_expander.set_pull_mode(0, true));
        assert!(!io_expander.set_high_impedance(0, false));
        assert!(!io_expander.write_value(0));
        assert!(!io_expander.digital_write(0, true));
        assert!(!io_expander.digital_read(0));
        assert!(!io_expander.reset_irq());
        assert!(!io_expander.disable_irq());
        assert!(!io_expander.enable_irq());
        let mut pi4ioe5v6408 = m5.pi4ioe5v6408();
        assert!(!pi4ioe5v6408.begin());
        assert!(!pi4ioe5v6408.set_direction(0, true));
        assert!(!pi4ioe5v6408.enable_pull(0, true));
        assert!(!pi4ioe5v6408.set_pull_mode(0, true));
        assert!(!pi4ioe5v6408.set_high_impedance(0, false));
        assert!(!pi4ioe5v6408.write_value(0));
        assert!(!pi4ioe5v6408.digital_write(0, true));
        assert!(!pi4ioe5v6408.digital_read(0));
        pi4ioe5v6408.reset_irq();
        pi4ioe5v6408.disable_irq();
        pi4ioe5v6408.enable_irq();
        m5.ex_i2c.set_port(0, 1, 2);
        assert!(!m5.ex_i2c.begin());
        assert!(!m5.ex_i2c.begin_with_port(0, 1, 2));
        assert!(!m5.ex_i2c.start(0x42, false, 100_000));
        assert!(!m5.ex_i2c.restart(0x42, true, 100_000));
        assert!(!m5.ex_i2c.write_byte(0xAA));
        assert!(!m5.ex_i2c.write(&[1, 2, 3]));
        assert!(!m5.ex_i2c.read(&mut [0_u8; 2], true));
        assert!(!m5.ex_i2c.write_register(0x42, 0x10, &[1], 100_000));
        assert!(!m5.ex_i2c.read_register(0x42, 0x10, &mut [0_u8; 2], 100_000));
        assert!(!m5.ex_i2c.write_register8(0x42, 0x10, 1, 100_000));
        assert_eq!(m5.ex_i2c.read_register8(0x42, 0x10, 100_000), 0);
        assert!(!m5.ex_i2c.bit_on(0x42, 0x10, 0x01, 100_000));
        assert!(!m5.ex_i2c.bit_off(0x42, 0x10, 0x01, 100_000));
        assert!(!m5.ex_i2c.scan_address(0x42, 100_000));
        assert_eq!(m5.ex_i2c.scan(100_000), [false; 120]);
        assert!(!m5.ex_i2c.stop());
        assert!(!m5.ex_i2c.release());

        let mut device = I2cDevice::external(0x42, 100_000);
        assert_eq!(device.address(), 0x42);
        assert_eq!(device.clock_hz(), 100_000);
        assert!(!device.is_bus_enabled());
        device.set_address(0x43);
        device.set_clock(400_000);
        device.set_port(I2cBus::INTERNAL);
        assert_eq!(device.address(), 0x43);
        assert_eq!(device.clock_hz(), 400_000);
        assert_eq!(device.bus(), I2cBus::INTERNAL);
        assert!(!device.write_register8(0x10, 0x01));
        assert_eq!(device.read_register8(0x10), 0);
        assert!(!device.write_register8_array(&[0x10, 0x01, 0x11, 0x02]));
        assert!(!device.write_register8_array(&[0x10]));
        assert!(!device.write_register8_pairs(&[(0x10, 0x01)]));
        assert!(!device.write_register(0x10, &[1, 2]));
        assert!(!device.read_register(0x10, &mut [0_u8; 2]));
        assert!(!device.bit_on(0x10, 0x01));
        assert!(!device.bit_off(0x10, 0x01));
    }

    #[test]
    fn begin_with_config_uses_host_stub() {
        let config = M5UnifiedConfig {
            led_brightness: 32,
            external_imu: true,
            external_rtc: true,
            ..M5UnifiedConfig::default()
        };
        let m5 = M5Unified::begin_with_config(&config).expect("host stub begin should succeed");
        assert_eq!(m5.display.width(), 320);
        assert_eq!(config.to_raw().fallback_board, -1);

        let config = M5UnifiedConfig {
            fallback_board: Some(Board::M5AtomS3Lite),
            ..M5UnifiedConfig::default()
        };
        assert_eq!(config.to_raw().fallback_board, 137);
    }

    #[test]
    fn button_state_helpers_compile_on_host() {
        let m5 = M5Unified::begin().expect("host stub begin should succeed");
        let button = m5.buttons.a();

        assert!(m5.buttons.get(0).is_some());
        assert!(m5.buttons.get(4).is_some());
        assert!(m5.buttons.get(5).is_none());
        assert_eq!(ButtonState::from_raw(0), ButtonState::NoChange);
        assert_eq!(ButtonState::Raw(9).raw(), 9);
        assert!(!button.is_pressed());
        assert!(button.is_released());
        assert!(!button.was_pressed());
        assert!(!button.was_released());
        assert!(!button.was_released_after_hold());
        assert!(!button.was_clicked());
        assert!(!button.was_single_clicked());
        assert!(!button.was_double_clicked());
        assert!(!button.was_hold());
        assert!(!button.is_holding());
        assert!(!button.was_change_pressed());
        assert!(!button.was_decide_click_count());
        #[allow(deprecated)]
        let was_decied_click_count = button.was_decied_click_count();
        assert!(!was_decied_click_count);
        assert_eq!(button.click_count(), 0);
        assert!(!button.was_release_for_ms(10));
        #[allow(deprecated)]
        let was_releasefor_ms = button.was_releasefor_ms(10);
        assert!(!was_releasefor_ms);
        assert!(!button.pressed_for_ms(10));
        assert!(button.released_for_ms(10));
        button.set_debounce_thresh_ms(12);
        button.set_hold_thresh_ms(600);
        button.set_raw_state(20, true);
        button.set_state_at(30, ButtonState::Clicked);
        assert_eq!(button.state(), ButtonState::NoChange);
        assert_eq!(button.last_change_ms(), 0);
        assert_eq!(button.debounce_thresh_ms(), 10);
        assert_eq!(button.hold_thresh_ms(), 500);
        assert_eq!(button.update_msec(), 0);
    }

    #[test]
    fn touch_state_helpers_compile_on_host() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert!(!m5.touch.is_enabled());
        assert!(!m5.touch.is_pressed());
        assert_eq!(m5.touch.count(), 0);
        assert!(m5.touch.points().is_empty());
        assert_eq!(m5.touch.raw_point(0), None);
        assert!(m5.touch.raw_points().is_empty());
        assert_eq!(m5.touch.detail(0), None);
        assert!(m5.touch.details().is_empty());
        m5.touch.set_hold_thresh_ms(500);
        m5.touch.set_flick_thresh_px(8);

        assert_eq!(TouchState::from_raw(0b1111), TouchState::DragBegin);
        assert_eq!(TouchState::Raw(0x80).raw(), 0x80);
        assert!(TouchState::TouchBegin.is_pressed());
        assert!(TouchState::TouchEnd.was_released());
        assert!(TouchState::TouchEnd.was_clicked());
        assert!(TouchState::HoldBegin.is_holding());
        assert!(TouchState::HoldBegin.was_hold());
        assert!(TouchState::FlickBegin.was_flick_start());
        assert!(TouchState::FlickBegin.is_flicking());
        assert!(TouchState::FlickEnd.was_flicked());
        assert!(TouchState::DragBegin.was_drag_start());
        assert!(TouchState::DragBegin.is_dragging());
        assert!(TouchState::DragEnd.was_dragged());

        let detail = TouchDetail {
            x: 12,
            y: 15,
            prev_x: 10,
            prev_y: 11,
            base_x: 4,
            base_y: 5,
            state: TouchState::DragBegin,
            is_pressed: true,
            was_pressed: true,
            was_clicked: false,
            was_released: false,
            was_hold: false,
            is_holding: true,
            click_count: 2,
            ..TouchDetail::default()
        };
        assert!(detail.is_pressed());
        assert!(detail.was_pressed());
        assert!(!detail.was_clicked());
        assert!(!detail.was_released());
        assert!(detail.is_holding());
        assert!(!detail.was_hold());
        assert_eq!(detail.click_count(), 2);
        assert_eq!(detail.delta_x(), 2);
        assert_eq!(detail.delta_y(), 4);
        assert_eq!(detail.delta(), (2, 4));
        assert_eq!(detail.distance_x(), 8);
        assert_eq!(detail.distance_y(), 10);
        assert_eq!(detail.distance(), (8, 10));
        assert!(detail.was_drag_start());
        assert!(detail.is_dragging());
    }

    #[test]
    fn display_and_rtc_example_helpers_compile_on_host() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert!(m5.rtc.begin());
        assert!(m5.rtc.init());
        assert!(m5.rtc.begin_for_board(Board::M5AtomS3Lite));
        assert!(m5.rtc.init_for_board(Board::M5AtomS3Lite));
        assert!(m5.rtc.is_enabled());
        assert!(m5.display.set_font(DisplayFont::Ascii8x16));
        assert!(m5.display.set_font(DisplayFont::LgfxJapanGothic12));
        assert!(m5.display.set_font(DisplayFont::DejaVu18));
        m5.display.set_text_scroll(true);
        m5.display.set_epd_mode(EpdMode::Fastest);
        assert!(write!(&mut m5.display, "primary value={}", 7).is_ok());
        m5.rtc.set_system_time_from_rtc();
        assert!(!m5.rtc.volt_low());
        let date = m5.rtc.get_date().expect("host stub date should exist");
        let time = m5.rtc.get_time().expect("host stub time should exist");
        let datetime = DateTime::from_date_time(date, time);
        assert_eq!(datetime.date(), date);
        assert_eq!(datetime.time(), time);
        assert!(m5.rtc.set_date(date));
        assert!(m5.rtc.set_time(time));
        assert_eq!(m5.rtc.set_timer_irq_ms(250), 250);
        assert_eq!(m5.rtc.set_alarm_irq_after_seconds(2), 2);
        assert_eq!(m5.rtc.set_alarm_irq_time(time), 0);
        assert!(!m5.rtc.irq_status());
        m5.rtc.clear_irq();
        m5.rtc.disable_irq();

        let mut pcf8563 = m5.rtc.pcf8563();
        assert_eq!(pcf8563.kind(), RtcDeviceKind::Pcf8563);
        assert!(!pcf8563.begin());
        assert!(!pcf8563.init());
        assert!(pcf8563.get_datetime().is_none());
        assert!(pcf8563.get_date().is_none());
        assert!(pcf8563.get_time().is_none());
        assert!(!pcf8563.set_datetime(datetime));
        assert!(!pcf8563.set_date(date));
        assert!(!pcf8563.set_time(time));
        assert!(!pcf8563.volt_low());
        assert_eq!(pcf8563.set_timer_irq_ms(250), 0);
        assert_eq!(pcf8563.set_alarm_irq(datetime), -1);
        assert_eq!(pcf8563.set_alarm_irq_time(time), -1);
        assert!(!pcf8563.irq_status());
        pcf8563.clear_irq();
        pcf8563.disable_irq();
        assert_eq!(m5.rtc.rx8130().kind(), RtcDeviceKind::Rx8130);
        assert_eq!(m5.rtc.power_hub().kind(), RtcDeviceKind::PowerHub);

        let mut display = m5.display(0).expect("host stub display should exist");
        assert_eq!(display.width(), 320);
        assert_eq!(display.height(), 240);
        assert_eq!(display.rotation(), 0);
        display.clear();
        display.fill_screen(colors::BLACK);
        display.set_cursor(0, 0);
        display.set_text_size(1);
        display.set_text_color(colors::WHITE, colors::BLACK);
        display.set_rotation(1);
        display.set_color(colors::YELLOW);
        assert!(write!(&mut display, "indexed value={}", 3).is_ok());
        assert_eq!(display.println("indexed display"), Ok(()));
        assert_eq!(display.draw_string("indexed", 0, 0), Ok(0));
        display.transaction(|display| {
            display.write_pixel(0, 0, colors::WHITE);
            display.draw_pixel(1, 1, colors::RED);
            display.draw_line(0, 0, 8, 8, colors::GREEN);
            display.draw_rect(0, 0, 6, 6, colors::CYAN);
            display.fill_rect(0, 0, 4, 4, colors::BLUE);
            display.draw_circle(8, 8, 3, colors::MAGENTA);
            display.fill_circle(8, 8, 2, colors::GREEN);
        });
    }

    #[test]
    fn log_configuration_helpers_compile_on_host() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert!(m5.log.set_enable_color(LogTarget::Serial, true));
        assert!(m5.log.enable_color(LogTarget::Serial));
        assert!(m5.log.set_log_level(LogTarget::Display, LogLevel::Debug));
        assert!(m5.log.log_level(LogTarget::Display).is_some());
        assert_eq!(m5.log.set_suffix(LogTarget::Callback, ""), Ok(true));
        assert_eq!(
            Log::path_to_file_name("/tmp/example/source.cpp"),
            Ok("source.cpp".to_string())
        );
        m5.log.println_empty();
        assert!(write!(&mut m5.log, "formatted log {}", 42).is_ok());
        m5.log.dump(&[0, 1, 2, 3], LogLevel::Info);
        assert!(m5.log.clear_callback());
    }

    #[test]
    fn sd_helpers_compile_on_host() {
        let config = SdSpiConfig {
            pin_sclk: 18,
            pin_mosi: 23,
            pin_miso: 19,
            pin_cs: 4,
            ..SdSpiConfig::default()
        };
        assert_eq!(SD_MOUNT_PATH, "/sdcard");
        assert_eq!(
            SdCard::path_for("sound.wav"),
            std::path::PathBuf::from("/sdcard/sound.wav")
        );
        assert_eq!(
            SdCard::path_for("/tmp/sound.wav"),
            std::path::PathBuf::from("/tmp/sound.wav")
        );
        assert!(!sd_begin());
        assert!(!sd_begin_with_config(&config));
        assert!(!sd_is_mounted());
        assert!(SdCard::mount().is_err());
        assert!(SdCard::mount_with_config(&config).is_err());
        sd_end();
    }

    #[test]
    fn axp2101_irq_helpers_compile_on_host() {
        let mut m5 = M5Unified::begin().expect("host stub begin should succeed");
        assert!(!m5.power.begin());
        assert_eq!(m5.power.pmic_type(), PowerType::Unknown);
        assert_eq!(m5.power.charge_state(), ChargeState::Unknown);
        assert_eq!(m5.power.vbus_voltage_mv(), None);
        assert_eq!(m5.power.battery_current_ma(), 0);
        assert_eq!(m5.power.ext_voltage_mv(ExtPortMask::PA), 0.0);
        assert_eq!(m5.power.ext_current_ma(ExtPortMask::PA), 0.0);
        assert_eq!(m5.power.key_state(), 0);
        m5.power.set_led(32);
        m5.power
            .set_ext_output(true, ExtPortMask::PA | ExtPortMask::PB1);
        m5.power.set_ext_power(true);
        assert!(!m5.power.ext_output());
        m5.power.set_usb_output(true);
        assert!(!m5.power.usb_output());
        m5.power.set_battery_charge(true);
        m5.power.set_charge_current_ma(500);
        m5.power.set_charge_voltage_mv(4_200);
        m5.power.set_vibration(0);
        m5.power.set_ext_port_bus_config(ExtPortBusConfig {
            voltage_mv: 5_000,
            current_limit_ma: 100,
            enable: true,
            direction_output: true,
        });
        m5.power.timer_sleep_seconds(0);
        m5.power.timer_sleep_time(Time {
            hour: 6,
            minute: 30,
            second: 0,
        });
        m5.power.timer_sleep_date_time(
            Date {
                year: 2026,
                month: 5,
                day: 18,
                weekday: Some(1),
            },
            Time {
                hour: 6,
                minute: 30,
                second: 0,
            },
        );
        m5.power.deep_sleep_us(0, false);
        m5.power.light_sleep_us(0, false);
        m5.power.power_off();
        let axp192 = m5.power.axp192();
        assert!(!axp192.begin());
        assert_eq!(axp192.battery_level(), None);
        assert!(!axp192.set_battery_charge(true));
        assert!(!axp192.set_charge_current_ma(500));
        assert!(!axp192.set_charge_voltage_mv(4_200));
        assert!(!axp192.is_charging());
        assert!(!axp192.set_dcdc1_mv(Some(3_300)));
        assert!(!axp192.set_dcdc2_mv(Some(1_200)));
        assert!(!axp192.set_dcdc3_mv(None));
        assert!(!axp192.set_ldo0_mv(Some(3_300)));
        assert!(!axp192.set_ldo2_mv(Some(3_300)));
        assert!(!axp192.set_ldo3_mv(None));
        assert!(!axp192.set_gpio(0, true));
        assert!(!axp192.set_gpio0(true));
        assert!(!axp192.set_gpio1(false));
        assert!(!axp192.set_gpio2(true));
        assert!(!axp192.set_gpio3(false));
        assert!(!axp192.set_gpio4(true));
        assert!(!axp192.set_adc_state(true));
        assert!(!axp192.set_adc_rate(0));
        assert!(!axp192.set_exten(true));
        assert!(!axp192.set_backup(true));
        assert!(!axp192.is_acin());
        assert!(!axp192.is_vbus());
        assert!(!axp192.battery_present());
        assert!(!axp192.exten());
        assert_eq!(axp192.battery_voltage_v(), 0.0);
        assert_eq!(axp192.battery_discharge_current_ma(), 0.0);
        assert_eq!(axp192.battery_charge_current_ma(), 0.0);
        assert_eq!(axp192.battery_power_mw(), 0.0);
        assert_eq!(axp192.acin_voltage_v(), 0.0);
        assert_eq!(axp192.acin_current_ma(), 0.0);
        assert_eq!(axp192.vbus_voltage_v(), 0.0);
        assert_eq!(axp192.vbus_current_ma(), 0.0);
        assert_eq!(axp192.aps_voltage_v(), 0.0);
        assert_eq!(axp192.internal_temperature_c(), 0.0);
        assert_eq!(axp192.pek_press(), Axp192PekPress::None);
        assert_eq!(Axp192PekPress::Both.raw(), 3);
        assert_eq!(Axp192PekPress::from_raw(9), Axp192PekPress::Raw(9));
        assert!(!axp192.power_off());
        let aw = m5.power.aw32001();
        assert!(!aw.begin());
        assert!(!aw.set_battery_charge(true));
        assert!(!aw.set_charge_current_ma(100));
        assert!(!aw.set_charge_voltage_mv(4_200));
        assert!(!aw.is_charging());
        assert_eq!(aw.charge_current_ma(), None);
        assert_eq!(aw.charge_voltage_mv(), None);
        assert_eq!(aw.charge_status(), Aw32001ChargeStatus::Unknown);
        assert_eq!(Aw32001ChargeStatus::Charging.raw(), 2);
        assert_eq!(
            Aw32001ChargeStatus::from_raw(77),
            Aw32001ChargeStatus::Raw(77)
        );
        let bq = m5.power.bq27220();
        assert!(!bq.begin());
        assert_eq!(bq.current_ma(), 0);
        assert_eq!(bq.voltage_mv(), 0);
        assert_eq!(bq.current_a(), 0.0);
        assert_eq!(bq.voltage_v(), 0.0);
        let ina = m5.power.ina226();
        assert!(!ina.begin());
        let ina_config = Ina226Config {
            shunt_res_ohm: 0.005,
            max_expected_current_a: 2.0,
            sampling_rate: Ina226Sampling::Rate16,
            shunt_conversion_time: Ina226ConversionTime::Us1100,
            bus_conversion_time: Ina226ConversionTime::Us1100,
            mode: Ina226Mode::ShuntAndBus,
        };
        assert!(!ina.configure(ina_config));
        assert_eq!(Ina226Sampling::Rate16.raw(), 2);
        assert_eq!(Ina226ConversionTime::Us1100.raw(), 4);
        assert_eq!(Ina226Mode::ShuntAndBus.raw(), 7);
        assert_eq!(Ina226Mode::from_raw(4), Ina226Mode::Raw(4));
        assert_eq!(ina.bus_voltage_v(), 0.0);
        assert_eq!(ina.shunt_voltage_v(), 0.0);
        assert_eq!(ina.shunt_current_a(), 0.0);
        assert_eq!(ina.power_w(), 0.0);
        let ina3221 = m5.power.ina3221(0);
        assert_eq!(ina3221.index(), 0);
        assert_eq!(Ina3221::CHANNEL_COUNT, 3);
        assert!(!ina3221.begin());
        assert!(!ina3221.set_shunt_res_milliohm(0, 10));
        assert_eq!(ina3221.bus_voltage_v(0), 0.0);
        assert_eq!(ina3221.shunt_voltage_v(0), 0.0);
        assert_eq!(ina3221.current_a(0), 0.0);
        assert_eq!(ina3221.bus_voltage_mv(0), 0);
        assert_eq!(ina3221.shunt_voltage_mv(0), 0);
        let ip5306 = m5.power.ip5306();
        assert!(!ip5306.begin());
        assert_eq!(ip5306.battery_level(), None);
        assert!(!ip5306.set_battery_charge(true));
        assert!(!ip5306.set_charge_current_ma(500));
        assert!(!ip5306.set_charge_voltage_mv(4_200));
        assert!(!ip5306.is_charging());
        assert!(!ip5306.set_power_boost_keep_on(true));
        let py32 = m5.power.py32pmic();
        assert!(!py32.begin());
        assert!(!py32.set_ext_output(true));
        assert!(!py32.set_battery_charge(true));
        assert!(!py32.set_charge_current_ma(100));
        assert!(!py32.set_charge_voltage_mv(4_200));
        assert!(!py32.is_charging());
        assert_eq!(py32.charge_current_ma(), None);
        assert_eq!(py32.charge_voltage_mv(), None);
        assert_eq!(py32.pek_press(), Py32PmicPekPress::None);
        assert_eq!(Py32PmicPekPress::Short.raw(), 2);
        assert_eq!(Py32PmicPekPress::from_raw(7), Py32PmicPekPress::Raw(7));
        assert!(!py32.power_off());
        let axp = m5.power.axp2101();
        assert!(!axp.begin());
        assert_eq!(axp.battery_level(), None);
        assert!(!axp.set_battery_charge(true));
        assert!(!axp.set_pre_charge_current_ma(100));
        assert!(!axp.set_charge_current_ma(500));
        assert!(!axp.set_charge_voltage_mv(4_200));
        assert_eq!(axp.charge_status(), Axp2101ChargeStatus::Unavailable);
        assert_eq!(Axp2101ChargeStatus::Charging.raw(), 1);
        assert_eq!(
            Axp2101ChargeStatus::from_raw(99),
            Axp2101ChargeStatus::Raw(99)
        );
        assert!(!axp.is_charging());
        assert!(!axp.set_aldo1_mv(Some(3_300)));
        assert!(!axp.set_aldo2_mv(Some(3_300)));
        assert!(!axp.set_aldo3_mv(Some(3_300)));
        assert!(!axp.set_aldo4_mv(None));
        assert!(!axp.set_bldo1_mv(Some(1_800)));
        assert!(!axp.set_bldo2_mv(None));
        assert!(!axp.set_dldo1_mv(Some(3_300)));
        assert!(!axp.set_dldo2_mv(None));
        assert!(!axp.aldo1_enabled());
        assert!(!axp.aldo2_enabled());
        assert!(!axp.aldo3_enabled());
        assert!(!axp.aldo4_enabled());
        assert!(!axp.bldo1_enabled());
        assert!(!axp.bldo2_enabled());
        assert!(!axp.set_adc_state(true));
        assert!(!axp.set_adc_rate(0));
        assert!(!axp.set_backup(true));
        assert!(!axp.is_acin());
        assert!(!axp.is_vbus());
        assert!(!axp.battery_present());
        assert_eq!(axp.battery_voltage_v(), 0.0);
        assert_eq!(axp.battery_discharge_current_ma(), 0.0);
        assert_eq!(axp.battery_charge_current_ma(), 0.0);
        assert_eq!(axp.battery_power_mw(), 0.0);
        assert_eq!(axp.acin_voltage_v(), 0.0);
        assert_eq!(axp.acin_current_ma(), 0.0);
        assert_eq!(axp.vbus_voltage_v(), 0.0);
        assert_eq!(axp.vbus_current_ma(), 0.0);
        assert_eq!(axp.ts_voltage_v(), 0.0);
        assert_eq!(axp.aps_voltage_v(), 0.0);
        assert_eq!(axp.internal_temperature_c(), 0.0);
        assert_eq!(axp.pek_press(), Axp2101PekPress::None);
        assert_eq!(Axp2101PekPress::Both.raw(), 3);
        assert_eq!(Axp2101PekPress::from_raw(4), Axp2101PekPress::Raw(4));
        assert!(!axp.power_off());
        let mask = Axp2101::IRQ_BAT_CHG_UNDER_TEMP | Axp2101::IRQ_VBUS_INSERT;
        assert!(!axp.disable_irq(Axp2101::IRQ_ALL));
        assert!(!axp.enable_irq(mask));
        assert!(!axp.clear_irq_statuses());
        let status = axp.irq_statuses();
        assert_eq!(status.raw, 0);
        assert!(status.is_empty());
        assert!(!status.battery_charger_under_temperature());
        assert!(!status.vbus_insert());

        let status = Axp2101IrqStatus {
            raw: Axp2101::IRQ_BAT_WORK_UNDER_TEMP
                | Axp2101::IRQ_BAT_CHG_UNDER_TEMP
                | Axp2101::IRQ_GAUGE_NEW_SOC
                | Axp2101::IRQ_WARNING_LEVEL2
                | Axp2101::IRQ_PKEY_SHORT_PRESS
                | Axp2101::IRQ_BAT_INSERT
                | Axp2101::IRQ_VBUS_INSERT
                | Axp2101::IRQ_BAT_OVER_VOLTAGE
                | Axp2101::IRQ_BAT_CHG_START
                | Axp2101::IRQ_LDO_OVER_CURR
                | Axp2101::IRQ_WDT_EXPIRE,
        };
        assert!(!status.is_empty());
        assert!(status.contains(Axp2101::IRQ_PKEY_SHORT_PRESS));
        assert!(status.battery_work_under_temperature());
        assert!(!status.battery_work_over_temperature());
        assert!(status.battery_charger_under_temperature());
        assert!(!status.battery_charger_over_temperature());
        assert!(status.gauge_new_soc());
        assert!(!status.gauge_watchdog_timeout());
        assert!(!status.warning_level1());
        assert!(status.warning_level2());
        assert!(!status.pkey_positive_edge());
        assert!(!status.pkey_negative_edge());
        assert!(!status.pkey_long_press());
        assert!(status.pkey_short_press());
        assert!(!status.battery_remove());
        assert!(status.battery_insert());
        assert!(!status.vbus_remove());
        assert!(status.vbus_insert());
        assert!(status.battery_over_voltage());
        assert!(!status.charger_timer_expired());
        assert!(!status.die_over_temperature());
        assert!(status.battery_charge_start());
        assert!(!status.battery_charge_done());
        assert!(!status.batfet_over_current());
        assert!(status.ldo_over_current());
        assert!(status.watchdog_expired());
    }
}
