use crate::vars::{FunctionCode, EXCEPTION_OFFSET, MEI_READ_DEVICE_ID};

const CRC_TABLE: [u16; 256] = [
    0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741,
    0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
    0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941,
    0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
    0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341,
    0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
    0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40,
    0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
    0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41,
    0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
    0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141,
    0x6300, 0xa3c1, 0xa281, 0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
    0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01, 0x6ac0, 0x6b80, 0xab41,
    0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
    0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541,
    0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
    0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741,
    0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
    0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941,
    0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
    0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341,
    0x4100, 0x81c1, 0x8081, 0x4040,
];

pub fn crc(data: &[u8]) -> u16 {
    crc_with_seed(data, 0xffff)
}

/// Update an in-progress CRC by feeding `data`. Pass `0xffff` for a fresh
/// computation, or the previous result to extend a running CRC by additional
/// bytes (mirrors njs-modbus `crc(data, seed)`).
pub fn crc_with_seed(data: &[u8], seed: u16) -> u16 {
    let mut crc = seed;
    for &byte in data {
        crc = CRC_TABLE[((crc ^ byte as u16) & 0xff) as usize] ^ (crc >> 8);
    }
    crc
}

pub fn lrc(data: &[u8]) -> u8 {
    let sum: u8 = data.iter().copied().fold(0u8, |a, b| a.wrapping_add(b));
    (0u8).wrapping_sub(sum)
}

fn in_range(n: u16, (min, max): (u16, u16)) -> bool {
    n >= min && n <= max
}

pub fn check_range(value: &[u16], range: &[(u16, u16)]) -> bool {
    if range.is_empty() {
        return true;
    }
    for &(min, max) in range {
        if min <= max && value.iter().all(|&v| in_range(v, (min, max))) {
            return true;
        }
    }
    false
}

pub fn get_three_point_five_t(baud_rate: u32, approximation: u32) -> f64 {
    bits_to_ms(baud_rate, approximation as f64)
}

/// Convert a number of bits to milliseconds at the given baud rate.
///
/// Mirrors njs-modbus `bitsToMs`. Used to derive Modbus RTU timing intervals
/// from bit counts — e.g. 38.5 bits = 3.5 character times at 11 bits/char (t3.5
/// inter-frame silence), or 16.5 bits = 1.5 character times (t1.5
/// inter-character timeout), per Modbus V1.02 §2.5.1.1.
pub fn bits_to_ms(baud_rate: u32, bits: f64) -> f64 {
    (bits * 1000.0) / baud_rate as f64
}

/// Returns true when `n` is in the unsigned-byte range `[0, 255]`.
///
/// Mirrors njs-modbus `isUint8`. Used for byte-level Modbus payload
/// validation (FC17 server-ID elements, FC43 object values, custom-FC
/// payloads).
pub fn is_uint8(n: i32) -> bool {
    (0..=255).contains(&n)
}

pub fn pack_coils(coils: &[bool], length: u16) -> Vec<u8> {
    let byte_count = ((length + 7) / 8) as usize;
    let mut data = Vec::with_capacity(1 + byte_count);
    data.push(byte_count as u8);
    for chunk in coils.chunks(8) {
        let mut byte = 0u8;
        for (bit_idx, &v) in chunk.iter().enumerate() {
            if v {
                byte |= 1 << bit_idx;
            }
        }
        data.push(byte);
    }
    data
}

pub fn parse_coils(data: &[u8], length: u16) -> Vec<bool> {
    let mut result = Vec::with_capacity(length as usize);
    for byte_idx in 0..((length + 7) / 8) as usize {
        let byte = data[1 + byte_idx];
        for bit_idx in 0..8 {
            let i = byte_idx * 8 + bit_idx;
            if i >= length as usize {
                break;
            }
            result.push((byte >> bit_idx) & 1 == 1);
        }
    }
    result
}

pub fn pack_registers(registers: &[u16], length: u16) -> Vec<u8> {
    let byte_count = (length * 2) as usize;
    let mut data = Vec::with_capacity(1 + byte_count);
    data.push(byte_count as u8);
    for reg in registers {
        data.extend_from_slice(&reg.to_be_bytes());
    }
    data
}

pub fn parse_registers(data: &[u8], length: u16) -> Vec<u16> {
    let mut result = Vec::with_capacity(length as usize);
    for i in 0..length {
        let idx = 1 + (i as usize) * 2;
        result.push(u16::from_be_bytes([data[idx], data[idx + 1]]));
    }
    result
}

/// Predictor result mirroring njs-modbus `PredictResult` discriminated union.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PredictResult {
    /// Function code is known and total RTU frame length (PDU + 2-byte CRC) is determined.
    Length(usize),
    /// Function code is known, but more bytes are required to decide
    /// (typically waiting on the byteCount byte or a variable-length tail).
    NeedMore,
    /// Function code is not in the standard tables — caller must defer to a
    /// registered `CustomFunctionCode` or treat this as a framing error.
    Unknown,
}

/// True when `fc` has a built-in framing predictor (standard FC, exception, or
/// FC 0x2B response).
pub fn is_standard_fc(fc: u8, is_response: bool) -> bool {
    if is_response && (fc & EXCEPTION_OFFSET) != 0 {
        return true;
    }
    if is_response {
        matches!(
            fc,
            0x05 | 0x06 | 0x0f | 0x10 | 0x16 | 0x01 | 0x02 | 0x03 | 0x04 | 0x11 | 0x17 | 0x2b
        )
    } else {
        matches!(fc, 0x01..=0x06 | 0x11 | 0x16 | 0x2b | 0x0f | 0x10 | 0x17)
    }
}

/// Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
///
/// Mirrors njs-modbus `predictRtuFrameLength` after the `PredictResult`
/// refactor. Returns:
/// - `Length(n)` — fc is known and length is determined.
/// - `NeedMore` — fc is known but more bytes are required.
/// - `Unknown` — fc is not in the standard tables; caller must consult any
///   registered `CustomFunctionCode` or treat as a framing error.
pub fn predict_rtu_frame_length(buffer: &[u8], is_response: bool) -> PredictResult {
    if buffer.len() < 2 {
        return PredictResult::NeedMore;
    }
    let fc = buffer[1];

    if is_response && (fc & EXCEPTION_OFFSET) != 0 {
        return PredictResult::Length(5);
    }

    let fixed = if is_response {
        match fc {
            0x05 | 0x06 | 0x0f | 0x10 => Some(8usize),
            0x16 => Some(10),
            _ => None,
        }
    } else {
        match fc {
            0x01..=0x06 => Some(8usize),
            0x11 => Some(4),
            0x16 => Some(10),
            0x2b => Some(7),
            _ => None,
        }
    };
    if let Some(n) = fixed {
        return PredictResult::Length(n);
    }

    let byte_count = if is_response {
        match fc {
            0x01 | 0x02 | 0x03 | 0x04 | 0x11 | 0x17 => Some((2usize, 5usize)),
            _ => None,
        }
    } else {
        match fc {
            0x0f | 0x10 => Some((6usize, 9usize)),
            0x17 => Some((10, 13)),
            _ => None,
        }
    };
    if let Some((offset, extra)) = byte_count {
        if buffer.len() <= offset {
            return PredictResult::NeedMore;
        }
        return PredictResult::Length(extra + buffer[offset] as usize);
    }

    if is_response && fc == FunctionCode::ReadDeviceIdentification.as_u8() {
        return predict_fc43_14_response(buffer);
    }

    PredictResult::Unknown
}

/// Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
/// response structure per Modbus V1.1b3 §6.21.
///
/// Layout (after unit and fc):
///   mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
///   [objId(1) objLen(1) objData(objLen)] × numObjs
///   CRC(2)
fn predict_fc43_14_response(buffer: &[u8]) -> PredictResult {
    if buffer.len() < 8 {
        return PredictResult::NeedMore;
    }
    if buffer[2] != MEI_READ_DEVICE_ID {
        return PredictResult::Unknown;
    }
    let num_objs = buffer[7] as usize;
    let mut offset = 8usize;
    for _ in 0..num_objs {
        if buffer.len() < offset + 2 {
            return PredictResult::NeedMore;
        }
        let obj_len = buffer[offset + 1] as usize;
        offset += 2 + obj_len;
    }
    PredictResult::Length(offset + 2)
}

/// Generate a cross-process-unique connection id with the given prefix.
///
/// Format: `{prefix}-{uuid-v4}`. Uses RFC 4122 UUID v4 (128-bit random)
/// so IDs are unique even when multiple processes start simultaneously.
/// Mirrors njs-modbus `crypto.randomUUID()`.
pub fn gen_connection_id(prefix: &str) -> String {
    format!("{prefix}-{}", uuid::Uuid::new_v4())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_crc_known_values() {
        // Test with empty data
        assert_eq!(crc(&[]), 0xffff);
        // Test with single byte: TABLE[0xFE] ^ 0x00FF = 0x8081 ^ 0x00FF = 0x807E
        assert_eq!(crc(&[0x01]), 0x807e);
        // Test self-consistency: CRC of [data + crc(data)] should match
        let data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0a];
        let result = crc(&data);
        let frame_crc = result.to_le_bytes();
        let mut full_frame = data.to_vec();
        full_frame.extend_from_slice(&frame_crc);
        assert_eq!(crc(&full_frame[..full_frame.len() - 2]), result);
    }

    #[test]
    fn test_crc_with_seed_chains_equivalently() {
        // Splitting `crc(a ++ b)` into `crc_with_seed(b, crc(a))` must yield
        // the identical result — this is the invariant the sliding-window
        // running CRC in rtu.rs relies on.
        let data = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x0a, 0x77, 0xff];
        let full = crc(&data);
        for split in 0..=data.len() {
            let (left, right) = data.split_at(split);
            let chained = crc_with_seed(right, crc(left));
            assert_eq!(chained, full, "split={split}");
        }
        // crc() must equal crc_with_seed(.., 0xffff).
        assert_eq!(crc(&data), crc_with_seed(&data, 0xffff));
    }

    #[test]
    fn test_lrc_known_values() {
        // Empty data
        assert_eq!(lrc(&[]), 0x00);
        // Single byte: ~0x01 + 1 = 0xFF
        assert_eq!(lrc(&[0x01]), 0xff);
        // Two bytes: (0x01 + 0x03) = 0x04, ~0x04 + 1 = 0xFC
        assert_eq!(lrc(&[0x01, 0x03]), 0xfc);
    }

    #[test]
    fn test_check_range_single() {
        assert!(check_range(&[5], &[(0, 10)]));
        assert!(!check_range(&[15], &[(0, 10)]));
        assert!(check_range(&[5, 8], &[(0, 10)]));
        assert!(!check_range(&[5, 15], &[(0, 10)]));
    }

    #[test]
    fn test_check_range_multiple() {
        // Multiple ranges - any match is ok
        assert!(check_range(&[5], &[(0, 3), (4, 10)]));
        assert!(!check_range(&[5], &[(0, 3), (6, 10)]));
    }

    #[test]
    fn test_check_range_empty() {
        // Empty range means no restriction
        assert!(check_range(&[999], &[]));
    }

    #[test]
    fn test_check_range_invalid() {
        // min > max is invalid, skip
        assert!(!check_range(&[5], &[(10, 0)]));
    }

    #[test]
    fn test_three_point_five_t() {
        assert_eq!(get_three_point_five_t(9600, 48), 5.0);
        assert_eq!(get_three_point_five_t(19200, 48), 2.5);
    }

    #[test]
    fn test_parse_coils() {
        // 3 coils: true, false, true
        let data = vec![0x01, 0b00000101];
        let coils = parse_coils(&data, 3);
        assert_eq!(coils, vec![true, false, true]);

        // 8 coils: all true
        let data = vec![0x01, 0xFF];
        let coils = parse_coils(&data, 8);
        assert_eq!(coils, vec![true; 8]);

        // 10 coils
        let data = vec![0x02, 0b00000101, 0b00000011];
        let coils = parse_coils(&data, 10);
        assert_eq!(
            coils,
            vec![true, false, true, false, false, false, false, false, true, true]
        );
    }

    #[test]
    fn test_pack_coils() {
        let coils = vec![
            true, false, true, false, false, false, false, false, true, true,
        ];
        let packed = pack_coils(&coils, 10);
        assert_eq!(packed, vec![0x02, 0b00000101, 0b00000011]);
    }

    #[test]
    fn test_pack_and_parse_coils_roundtrip() {
        let coils = vec![true, false, true, true, false, true, false, true];
        let packed = pack_coils(&coils, 8);
        let parsed = parse_coils(&packed, 8);
        assert_eq!(coils, parsed);
    }

    #[test]
    fn test_parse_registers() {
        let data = vec![0x04, 0x00, 0x01, 0xAB, 0xCD];
        let regs = parse_registers(&data, 2);
        assert_eq!(regs, vec![0x0001, 0xABCD]);
    }

    #[test]
    fn test_pack_registers() {
        let regs = vec![0x0001u16, 0xABCD];
        let packed = pack_registers(&regs, 2);
        assert_eq!(packed, vec![0x04, 0x00, 0x01, 0xAB, 0xCD]);
    }

    #[test]
    fn test_pack_and_parse_registers_roundtrip() {
        let regs = vec![0x1234u16, 0x5678, 0x9ABC];
        let packed = pack_registers(&regs, 3);
        let parsed = parse_registers(&packed, 3);
        assert_eq!(regs, parsed);
    }

    // ===== predict_rtu_frame_length =====

    #[test]
    fn test_predict_buffer_too_short_need_more() {
        assert_eq!(
            predict_rtu_frame_length(&[], false),
            PredictResult::NeedMore
        );
        assert_eq!(
            predict_rtu_frame_length(&[0x01], false),
            PredictResult::NeedMore
        );
    }

    #[test]
    fn test_predict_request_fc1_2_3_4_5_6_fixed_8() {
        for fc in [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06] {
            assert_eq!(
                predict_rtu_frame_length(&[0x01, fc], false),
                PredictResult::Length(8),
                "request fc=0x{:02x} should predict 8",
                fc
            );
        }
    }

    #[test]
    fn test_predict_request_fc17_fixed_4() {
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x11], false),
            PredictResult::Length(4)
        );
    }

    #[test]
    fn test_predict_request_fc22_fixed_10() {
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x16], false),
            PredictResult::Length(10)
        );
    }

    #[test]
    fn test_predict_request_fc43_fixed_7() {
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x2b], false),
            PredictResult::Length(7)
        );
    }

    #[test]
    fn test_predict_request_fc15_byte_count() {
        let buf = [0x01u8, 0x0f, 0x00, 0x00, 0x00, 0x0a, 0x02];
        assert_eq!(
            predict_rtu_frame_length(&buf, false),
            PredictResult::Length(11)
        );
    }

    #[test]
    fn test_predict_request_fc16_byte_count() {
        let buf = [0x01u8, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04];
        assert_eq!(
            predict_rtu_frame_length(&buf, false),
            PredictResult::Length(13)
        );
    }

    #[test]
    fn test_predict_request_fc23_byte_count() {
        let buf = [
            0x01u8, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01, 0x02,
        ];
        assert_eq!(
            predict_rtu_frame_length(&buf, false),
            PredictResult::Length(15)
        );
    }

    #[test]
    fn test_predict_request_byte_count_buffer_too_short() {
        let buf = [0x01u8, 0x0f, 0x00, 0x00, 0x00, 0x0a];
        assert_eq!(
            predict_rtu_frame_length(&buf, false),
            PredictResult::NeedMore
        );
    }

    #[test]
    fn test_predict_response_exception_returns_5() {
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x83], true),
            PredictResult::Length(5)
        );
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x90], true),
            PredictResult::Length(5)
        );
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0xab], true),
            PredictResult::Length(5)
        );
    }

    #[test]
    fn test_predict_response_fc5_6_15_16_fixed_8() {
        for fc in [0x05u8, 0x06, 0x0f, 0x10] {
            assert_eq!(
                predict_rtu_frame_length(&[0x01, fc], true),
                PredictResult::Length(8),
                "response fc=0x{:02x} should predict 8",
                fc
            );
        }
    }

    #[test]
    fn test_predict_response_fc22_fixed_10() {
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x16], true),
            PredictResult::Length(10)
        );
    }

    #[test]
    fn test_predict_response_fc1_2_3_4_byte_count() {
        for fc in [0x01u8, 0x02, 0x03, 0x04] {
            let buf = [0x01u8, fc, 0x04, 0x00, 0x00, 0x00, 0x00];
            assert_eq!(
                predict_rtu_frame_length(&buf, true),
                PredictResult::Length(9),
                "response fc=0x{:02x} byte_count=4 should predict 9",
                fc
            );
        }
    }

    #[test]
    fn test_predict_response_fc17_byte_count() {
        let buf = [0x01u8, 0x11, 0x03];
        assert_eq!(
            predict_rtu_frame_length(&buf, true),
            PredictResult::Length(8)
        );
    }

    #[test]
    fn test_predict_response_fc23_byte_count() {
        let buf = [0x01u8, 0x17, 0x04];
        assert_eq!(
            predict_rtu_frame_length(&buf, true),
            PredictResult::Length(9)
        );
    }

    #[test]
    fn test_predict_response_byte_count_buffer_too_short() {
        let buf = [0x01u8, 0x03];
        assert_eq!(
            predict_rtu_frame_length(&buf, true),
            PredictResult::NeedMore
        );
    }

    #[test]
    fn test_predict_unknown_fc_returns_unknown() {
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x99], false),
            PredictResult::Unknown
        );
    }

    #[test]
    fn test_predict_request_exception_path_not_taken() {
        // In a request, fc with high bit set is NOT treated as exception
        // (only responses use that path)
        assert_eq!(
            predict_rtu_frame_length(&[0x01, 0x83], false),
            PredictResult::Unknown
        );
    }

    // ===== predict_rtu_frame_length: FC 0x2B response (MEI 0x0E) =====

    #[test]
    fn test_predict_fc43_response_needs_at_least_8_bytes() {
        for buf in &[
            &[][..],
            &[0x01],
            &[0x01, 0x2b],
            &[0x01, 0x2b, 0x0e, 0x01, 0x83, 0x00, 0x00],
        ] {
            assert_eq!(
                predict_rtu_frame_length(buf, true),
                PredictResult::NeedMore,
                "buf len {} should be NeedMore",
                buf.len()
            );
        }
    }

    #[test]
    fn test_predict_fc43_response_unsupported_mei_is_unknown() {
        // MEI != 0x0E (e.g. CANopen 0x0D) — not handled by built-in predictor.
        let buf = [0x01u8, 0x2b, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x00];
        assert_eq!(predict_rtu_frame_length(&buf, true), PredictResult::Unknown);
    }

    #[test]
    fn test_predict_fc43_response_zero_objects() {
        // unit fc mei rdic conformity more nextId numObjs=0 [+ CRC(2)]
        let buf = [0x01u8, 0x2b, 0x0e, 0x01, 0x81, 0x00, 0x00, 0x00];
        // No objects, total length = 8 + 2 = 10
        assert_eq!(
            predict_rtu_frame_length(&buf, true),
            PredictResult::Length(10)
        );
    }

    #[test]
    fn test_predict_fc43_response_one_object() {
        // 1 object: id=0x00, len=4, data="abcd"
        let buf = [
            0x01u8, 0x2b, 0x0e, 0x01, 0x81, 0x00, 0x00, 0x01, 0x00, 0x04, b'a', b'b', b'c', b'd',
        ];
        // 8 header + (2 + 4) object + 2 CRC = 16
        assert_eq!(
            predict_rtu_frame_length(&buf, true),
            PredictResult::Length(16)
        );
    }

    #[test]
    fn test_predict_fc43_response_object_tail_missing_is_need_more() {
        // numObjs=1 declared, but only header + object-id present
        let buf = [0x01u8, 0x2b, 0x0e, 0x01, 0x81, 0x00, 0x00, 0x01, 0x00];
        assert_eq!(
            predict_rtu_frame_length(&buf, true),
            PredictResult::NeedMore
        );
    }

    // ===== is_standard_fc =====

    #[test]
    fn test_is_standard_fc_known_request() {
        assert!(is_standard_fc(0x03, false));
        assert!(is_standard_fc(0x0f, false));
        assert!(is_standard_fc(0x2b, false));
    }

    #[test]
    fn test_is_standard_fc_known_response() {
        assert!(is_standard_fc(0x03, true));
        assert!(is_standard_fc(0x2b, true));
    }

    #[test]
    fn test_is_standard_fc_exception_response() {
        assert!(is_standard_fc(0x83, true));
        assert!(is_standard_fc(0xab, true));
    }

    #[test]
    fn test_is_standard_fc_unknown_returns_false() {
        assert!(!is_standard_fc(0x65, false));
        // 0x64 has high bit clear and is not a known FC → false.
        assert!(!is_standard_fc(0x64, true));
    }

    // ===== gen_connection_id =====

    #[test]
    fn test_gen_connection_id_has_prefix() {
        let id = gen_connection_id("serial");
        assert!(id.starts_with("serial-"), "got {}", id);
    }

    #[test]
    fn test_gen_connection_id_is_unique() {
        let a = gen_connection_id("tcp-server");
        let b = gen_connection_id("tcp-server");
        assert_ne!(a, b);
    }

    #[test]
    fn test_gen_connection_id_different_prefixes() {
        let a = gen_connection_id("a");
        let b = gen_connection_id("b");
        assert!(a.starts_with("a-"));
        assert!(b.starts_with("b-"));
    }

    #[test]
    fn test_gen_connection_id_uuid_v4_format() {
        let id = gen_connection_id("tcp");
        // Format: tcp-{uuid-v4} where uuid is 8-4-4-4-12 hex chars
        let uuid_part = id.strip_prefix("tcp-").unwrap();
        let parts: Vec<&str> = uuid_part.split('-').collect();
        assert_eq!(parts.len(), 5, "expected 5 hyphen-separated parts");
        assert_eq!(parts[0].len(), 8);
        assert_eq!(parts[1].len(), 4);
        assert_eq!(parts[2].len(), 4);
        assert_eq!(parts[3].len(), 4);
        assert_eq!(parts[4].len(), 12);
        // Version nibble must be 4
        assert_eq!(
            parts[2].chars().next().unwrap(),
            '4',
            "UUID version nibble must be 4"
        );
        // Variant nibble must be 8, 9, a, or b
        let variant = parts[3].chars().next().unwrap();
        assert!(
            matches!(variant, '8' | '9' | 'a' | 'b'),
            "UUID variant nibble must be 8-9-a-b, got {}",
            variant
        );
    }
}
