use std::collections::HashSet;
use std::{mem, ptr};

use winapi::ctypes::c_void;
use winapi::shared::guiddef::*;
use winapi::shared::minwindef::*;
use winapi::shared::winerror::*;
use winapi::um::cfgmgr32::*;
use winapi::um::cguid::GUID_NULL;
use winapi::um::setupapi::*;
use winapi::um::winnt::{KEY_READ, REG_SZ};
use winapi::um::winreg::*;

use crate::{Error, ErrorKind, Result, SerialPortInfo, SerialPortType, UsbPortInfo};

/// takes normal Rust `str` and outputs a null terminated UTF-16 encoded string
fn as_utf16(utf8: &str) -> Vec<u16> {
    utf8.encode_utf16().chain(Some(0)).collect()
}

/// takes a UTF-16 encoded slice (null termination not required)
/// and converts to a UTF8 Rust string. Trailing null chars are removed
fn from_utf16_lossy_trimmed(utf16: &[u16]) -> String {
    String::from_utf16_lossy(utf16)
        .trim_end_matches(0 as char)
        .to_string()
}

/// According to the MSDN docs, we should use SetupDiGetClassDevs, SetupDiEnumDeviceInfo
/// and SetupDiGetDeviceInstanceId in order to enumerate devices.
/// https://msdn.microsoft.com/en-us/windows/hardware/drivers/install/enumerating-installed-devices
fn get_ports_guids() -> Result<Vec<GUID>> {
    // SetupDiGetClassDevs returns the devices associated with a particular class of devices.
    // We want the list of devices which are listed as COM ports (generally those that show up in the
    // Device Manager as "Ports (COM & LPT)" which is otherwise known as the "Ports" class).
    //
    // The list of system defined classes can be found here:
    // https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors
    let class_names = ["Ports", "Modem"];
    let mut guids: Vec<GUID> = Vec::new();
    for class_name in class_names {
        let class_name_w = as_utf16(class_name);
        let mut num_guids: DWORD = 1; // Initially assume that there is only 1 guid per name.
        let class_start_idx = guids.len(); // start idx for this name (for potential resize with multiple guids)

        // first attempt with size == 1, second with the size returned from the first try
        for _ in 0..2 {
            guids.resize(class_start_idx + num_guids as usize, GUID_NULL);
            let guid_buffer = &mut guids[class_start_idx..];
            // Find out how many GUIDs are associated with this class name.  num_guids will tell us how many there actually are.
            let res = unsafe {
                SetupDiClassGuidsFromNameW(
                    class_name_w.as_ptr(),
                    guid_buffer.as_mut_ptr(),
                    guid_buffer.len() as DWORD,
                    &mut num_guids,
                )
            };
            if res == FALSE {
                return Err(Error::new(
                    ErrorKind::Unknown,
                    "Unable to determine number of Ports GUIDs",
                ));
            }
            let len_cmp = guid_buffer.len().cmp(&(num_guids as usize));
            // under allocated
            if len_cmp == std::cmp::Ordering::Less {
                continue; // retry
            }
            // allocation > required len
            else if len_cmp == std::cmp::Ordering::Greater {
                guids.truncate(class_start_idx + num_guids as usize);
            }
            break; // next name
        }
    }
    Ok(guids)
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct HwidMatches<'hwid> {
    vid: &'hwid str,
    pid: &'hwid str,
    serial: Option<&'hwid str>,
    interface: Option<&'hwid str>,
}

impl<'hwid> HwidMatches<'hwid> {
    fn new(hwid: &'hwid str) -> Option<Self> {
        // When we match something, update this so that we are always looking forward
        let mut hwid_tail = hwid;

        // VID_(?P<vid>[[:xdigit:]]{4})
        let vid_start = hwid.find("VID_")?;

        // We won't match for hex characters here. That can be done when parsed.
        let vid = hwid_tail.get(vid_start + 4..vid_start + 8)?;
        hwid_tail = hwid_tail.get(vid_start + 8..)?;

        // [&+]PID_(?P<pid>[[:xdigit:]]{4})
        let pid = if hwid_tail.starts_with("&PID_") || hwid_tail.starts_with("+PID_") {
            // We will let the hex parser fail if there are not hex digits.
            hwid_tail.get(5..9)?
        } else {
            return None;
        };
        hwid_tail = hwid_tail.get(9..)?;

        // (?:[&+]MI_(?P<iid>[[:xdigit:]]{2})){0,1}
        let iid = if hwid_tail.starts_with("&MI_") || hwid_tail.starts_with("+MI_") {
            // We will let the hex parser fail if there are not hex digits.
            let iid = hwid_tail.get(4..6);
            hwid_tail = hwid_tail.get(6..).unwrap_or(hwid_tail);

            iid
        } else {
            None
        };

        // ([\\+](?P<serial>\w+))? with slightly modified check for alphanumeric characters instead
        // of regex word character
        //
        // TODO: Fix returning no serial number at all for devices without one. The previous regex
        // and the code below return the first thing from the intance ID. See issue #203.
        let serial = if hwid_tail.starts_with('\\') || hwid_tail.starts_with('+') {
            hwid_tail.get(1..).and_then(|tail| {
                let index = tail
                    .char_indices()
                    .find(|&(_, char)| !char.is_alphanumeric())
                    .map(|(index, _)| index)
                    .unwrap_or(tail.len());
                tail.get(..index)
            })
        } else {
            None
        };

        Some(Self {
            vid,
            pid,
            serial,
            interface: iid,
        })
    }
}

/// Windows usb port information can be determined by the port's HWID string.
///
/// This function parses the HWID string using regex, and returns the USB port
/// information if the hardware ID can be parsed correctly. The manufacturer
/// and product names cannot be determined from the HWID string, so those are
/// set as None.
///
/// For composite USB devices, the HWID string will be for the interface. In
/// this case, the parent HWID string must be provided so that the correct
/// serial number can be determined.
///
/// Some HWID examples are:
///   - MicroPython pyboard:    USB\VID_F055&PID_9802\385435603432
///   - BlackMagic GDB Server:  USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000
///   - BlackMagic UART port:   USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
///   - FTDI Serial Adapter:    FTDIBUS\VID_0403+PID_6001+A702TB52A\0000
fn parse_usb_port_info(hardware_id: &str, parent_hardware_id: Option<&str>) -> Option<UsbPortInfo> {
    let mut caps = HwidMatches::new(hardware_id)?;

    let interface = caps.interface.and_then(|m| u8::from_str_radix(m, 16).ok());

    if interface.is_some() {
        // If this is a composite device, we need to parse the parent's HWID to get the correct information.
        caps = HwidMatches::new(parent_hardware_id?)?;
    }

    Some(UsbPortInfo {
        vid: u16::from_str_radix(caps.vid, 16).ok()?,
        pid: u16::from_str_radix(caps.pid, 16).ok()?,
        serial_number: caps.serial.map(str::to_string),
        manufacturer: None,
        product: None,

        #[cfg(feature = "usbportinfo-interface")]
        interface,
    })
}

struct PortDevices {
    /// Handle to a device information set.
    hdi: HDEVINFO,

    /// Index used by iterator.
    dev_idx: DWORD,
}

impl PortDevices {
    // Creates PortDevices object which represents the set of devices associated with a particular
    // Ports class (given by `guid`).
    pub fn new(guid: &GUID) -> Self {
        PortDevices {
            hdi: unsafe { SetupDiGetClassDevsW(guid, ptr::null(), ptr::null_mut(), DIGCF_PRESENT) },
            dev_idx: 0,
        }
    }
}

impl Iterator for PortDevices {
    type Item = PortDevice;

    /// Iterator which returns a PortDevice from the set of PortDevices associated with a
    /// particular PortDevices class (guid).
    fn next(&mut self) -> Option<PortDevice> {
        let mut port_dev = PortDevice {
            hdi: self.hdi,
            devinfo_data: SP_DEVINFO_DATA {
                cbSize: mem::size_of::<SP_DEVINFO_DATA>() as DWORD,
                ClassGuid: GUID_NULL,
                DevInst: 0,
                Reserved: 0,
            },
        };
        let res =
            unsafe { SetupDiEnumDeviceInfo(self.hdi, self.dev_idx, &mut port_dev.devinfo_data) };
        if res == FALSE {
            None
        } else {
            self.dev_idx += 1;
            Some(port_dev)
        }
    }
}

impl Drop for PortDevices {
    fn drop(&mut self) {
        // Release the PortDevices object allocated in the constructor.
        unsafe {
            SetupDiDestroyDeviceInfoList(self.hdi);
        }
    }
}

struct PortDevice {
    /// Handle to a device information set.
    hdi: HDEVINFO,

    /// Information associated with this device.
    pub devinfo_data: SP_DEVINFO_DATA,
}

impl PortDevice {
    /// Retrieves the device instance id string associated with this device's parent.
    /// This is useful for determining the serial number of a composite USB device.
    fn parent_instance_id(&mut self) -> Option<String> {
        let mut result_buf = [0u16; MAX_PATH];
        let mut parent_device_instance_id = 0;

        let res =
            unsafe { CM_Get_Parent(&mut parent_device_instance_id, self.devinfo_data.DevInst, 0) };
        if res == CR_SUCCESS {
            let buffer_len = result_buf.len() - 1;
            let res = unsafe {
                CM_Get_Device_IDW(
                    parent_device_instance_id,
                    result_buf.as_mut_ptr(),
                    buffer_len as ULONG,
                    0,
                )
            };

            if res == CR_SUCCESS {
                Some(from_utf16_lossy_trimmed(&result_buf))
            } else {
                None
            }
        } else {
            None
        }
    }

    /// Retrieves the device instance id string associated with this device. Some examples of
    /// instance id strings are:
    /// * MicroPython Board:  USB\VID_F055&PID_9802\385435603432
    /// * FTDI USB Adapter:   FTDIBUS\VID_0403+PID_6001+A702TB52A\0000
    /// * Black Magic Probe (Composite device with 2 UARTS):
    ///   * GDB Port:       USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000
    ///   * UART Port:      USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
    ///
    /// Reference: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/device-instance-ids
    fn instance_id(&mut self) -> Option<String> {
        let mut result_buf = [0u16; MAX_DEVICE_ID_LEN];
        let working_buffer_len = result_buf.len() - 1; // always null terminated
        let mut desired_result_len = 0; // possibly larger than the buffer
        let res = unsafe {
            SetupDiGetDeviceInstanceIdW(
                self.hdi,
                &mut self.devinfo_data,
                result_buf.as_mut_ptr(),
                working_buffer_len as DWORD,
                &mut desired_result_len,
            )
        };
        if res == FALSE {
            // Try to retrieve hardware id property.
            self.property(SPDRP_HARDWAREID)
        } else {
            let actual_result_len = working_buffer_len.min(desired_result_len as usize);
            Some(from_utf16_lossy_trimmed(&result_buf[..actual_result_len]))
        }
    }

    /// Retrieves the problem status of this device. For example, `CM_PROB_DISABLED` indicates
    /// the device has been disabled in Device Manager.
    fn problem(&mut self) -> Option<ULONG> {
        let mut status = 0;
        let mut problem_number = 0;
        let res = unsafe {
            CM_Get_DevNode_Status(
                &mut status,
                &mut problem_number,
                self.devinfo_data.DevInst,
                0,
            )
        };
        if res == CR_SUCCESS {
            Some(problem_number)
        } else {
            None
        }
    }

    // Retrieves the port name (i.e. COM6) associated with this device.
    pub fn name(&mut self) -> String {
        // https://learn.microsoft.com/en-us/windows/win32/api/setupapi/nf-setupapi-setupdiopendevregkey
        let hkey = unsafe {
            SetupDiOpenDevRegKey(
                self.hdi,
                &mut self.devinfo_data,
                DICS_FLAG_GLOBAL,
                0,
                DIREG_DEV,
                KEY_READ,
            )
        };

        if hkey as *mut c_void == winapi::um::handleapi::INVALID_HANDLE_VALUE {
            // failed to open registry key. Return empty string as the failure case
            return String::new();
        }

        // https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regqueryvalueexw
        let mut port_name_buffer = [0u16; MAX_PATH];
        let buffer_byte_len = 2 * port_name_buffer.len() as DWORD;
        let mut byte_len = buffer_byte_len;
        let mut value_type = 0;

        let value_name = as_utf16("PortName");
        let err = unsafe {
            RegQueryValueExW(
                hkey,
                value_name.as_ptr(),
                ptr::null_mut(),
                &mut value_type,
                port_name_buffer.as_mut_ptr() as *mut u8,
                &mut byte_len,
            )
        };
        unsafe { RegCloseKey(hkey) };
        if FAILED(err) {
            // failed to query registry for some reason. Return empty string as the failure case
            return String::new();
        }
        // https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
        if value_type != REG_SZ || byte_len % 2 != 0 || byte_len > buffer_byte_len {
            // read something but it wasn't the expected registry type
            return String::new();
        }
        // len of u16 chars, not bytes
        let len = buffer_byte_len as usize / 2;
        let port_name = &port_name_buffer[0..len];

        from_utf16_lossy_trimmed(port_name)
    }

    // Determines the port_type for this device, and if it's a USB port populate the various fields.
    pub fn port_type(&mut self) -> SerialPortType {
        self.instance_id()
            .map(|s| (s, self.parent_instance_id())) // Get parent instance id if it exists.
            .and_then(|(d, p)| parse_usb_port_info(&d, p.as_deref()))
            .map(|mut info: UsbPortInfo| {
                info.manufacturer = self.property(SPDRP_MFG);
                info.product = self.property(SPDRP_FRIENDLYNAME);
                SerialPortType::UsbPort(info)
            })
            .unwrap_or(SerialPortType::Unknown)
    }

    // Retrieves a device property and returns it, if it exists. Returns None if the property
    // doesn't exist.
    fn property(&mut self, property_id: DWORD) -> Option<String> {
        let mut value_type = 0;
        let mut property_buf = [0u16; MAX_PATH];

        let res = unsafe {
            SetupDiGetDeviceRegistryPropertyW(
                self.hdi,
                &mut self.devinfo_data,
                property_id,
                &mut value_type,
                property_buf.as_mut_ptr() as PBYTE,
                property_buf.len() as DWORD,
                ptr::null_mut(),
            )
        };

        if res == FALSE || value_type != REG_SZ {
            return None;
        }

        // Using the unicode version of 'SetupDiGetDeviceRegistryProperty' seems to report the
        // entire mfg registry string. This typically includes some driver information that we should discard.
        // Example string: 'FTDI5.inf,%ftdi%;FTDI'
        from_utf16_lossy_trimmed(&property_buf)
            .split(';')
            .last()
            .map(str::to_string)
    }
}

/// Not all COM ports are listed under the "Ports" device class
/// The full list of COM ports is available from the registry at
/// HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
///
/// port of https://learn.microsoft.com/en-us/windows/win32/sysinfo/enumerating-registry-subkeys
fn get_registry_com_ports() -> HashSet<String> {
    let mut ports_list = HashSet::new();

    let reg_key = as_utf16("HARDWARE\\DEVICEMAP\\SERIALCOMM");
    let key_ptr = reg_key.as_ptr();
    let mut ports_key = std::ptr::null_mut();

    // SAFETY: ffi, all inputs are correct
    let open_res =
        unsafe { RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_ptr, 0, KEY_READ, &mut ports_key) };
    if SUCCEEDED(open_res) {
        let mut class_name_buff = [0u16; MAX_PATH];
        let mut class_name_size = MAX_PATH as u32;
        let mut sub_key_count = 0;
        let mut largest_sub_key = 0;
        let mut largest_class_string = 0;
        let mut num_key_values = 0;
        let mut longest_value_name = 0;
        let mut longest_value_data = 0;
        let mut size_security_desc = 0;
        let mut last_write_time = FILETIME {
            dwLowDateTime: 0,
            dwHighDateTime: 0,
        };
        // SAFETY: ffi, all inputs are correct
        let query_res = unsafe {
            RegQueryInfoKeyW(
                ports_key,
                class_name_buff.as_mut_ptr(),
                &mut class_name_size,
                std::ptr::null_mut(),
                &mut sub_key_count,
                &mut largest_sub_key,
                &mut largest_class_string,
                &mut num_key_values,
                &mut longest_value_name,
                &mut longest_value_data,
                &mut size_security_desc,
                &mut last_write_time,
            )
        };
        if SUCCEEDED(query_res) {
            for idx in 0..num_key_values {
                let mut val_name_buff = [0u16; MAX_PATH];
                let mut val_name_size = MAX_PATH as u32;
                let mut value_type = 0;
                let mut val_data = [0u16; MAX_PATH];
                let buffer_byte_len = 2 * val_data.len() as DWORD; // len doubled
                let mut byte_len = buffer_byte_len;

                // SAFETY: ffi, all inputs are correct
                let res = unsafe {
                    RegEnumValueW(
                        ports_key,
                        idx,
                        val_name_buff.as_mut_ptr(),
                        &mut val_name_size,
                        std::ptr::null_mut(),
                        &mut value_type,
                        val_data.as_mut_ptr() as *mut u8,
                        &mut byte_len,
                    )
                };
                if FAILED(res)
                    || value_type != REG_SZ // only valid for text values
                    || byte_len % 2 != 0 // out byte len should be a multiple of u16 size
                    || byte_len > buffer_byte_len
                {
                    break;
                }
                // key data is returned as u16
                // SAFETY: data_size is checked and pointer is valid
                let val_data = from_utf16_lossy_trimmed(unsafe {
                    let utf16_len = byte_len / 2; // utf16 len
                    std::slice::from_raw_parts(val_data.as_ptr(), utf16_len as usize)
                });
                ports_list.insert(val_data);
            }
        }
        // SAFETY: ffi, all inputs are correct
        unsafe { RegCloseKey(ports_key) };
    }
    ports_list
}

/// List available serial ports on the system.
pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
    let mut ports = Vec::new();
    for guid in get_ports_guids()? {
        let port_devices = PortDevices::new(&guid);
        for mut port_device in port_devices {
            // Ignore nonfunctional devices
            if port_device.problem() != Some(0) {
                continue;
            }

            let port_name = port_device.name();

            debug_assert!(
                port_name.as_bytes().last().map_or(true, |c| *c != b'\0'),
                "port_name has a trailing nul: {:?}",
                port_name
            );

            // This technique also returns parallel ports, so we filter these out.
            if port_name.starts_with("LPT") {
                continue;
            }

            ports.push(SerialPortInfo {
                port_name,
                port_type: port_device.port_type(),
            });
        }
    }
    // ports identified through the registry have no additional information
    let mut raw_ports_set = get_registry_com_ports();
    if raw_ports_set.len() > ports.len() {
        // remove any duplicates. HashSet makes this relatively cheap
        for port in ports.iter() {
            raw_ports_set.remove(&port.port_name);
        }
        // add remaining ports as "unknown" type
        for raw_port in raw_ports_set {
            ports.push(SerialPortInfo {
                port_name: raw_port,
                port_type: SerialPortType::Unknown,
            })
        }
    }
    Ok(ports)
}

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

    use quickcheck_macros::quickcheck;

    #[test]
    fn from_utf16_lossy_trimmed_trimming_empty() {
        assert_eq!("", from_utf16_lossy_trimmed(&[]));
        assert_eq!("", from_utf16_lossy_trimmed(&[0]));
    }

    #[test]
    fn from_utf16_lossy_trimmed_trimming() {
        let test_str = "Testing";
        let wtest_str: Vec<u16> = as_utf16(test_str);
        let wtest_str_trailing = wtest_str
            .iter()
            .copied()
            .chain([0, 0, 0, 0]) // add some null chars
            .collect::<Vec<_>>();
        let and_back = from_utf16_lossy_trimmed(&wtest_str_trailing);

        assert_eq!(test_str, and_back);
    }

    // Check that passing some random data to HwidMatches::new() does not cause a panic.
    #[quickcheck]
    fn quickcheck_hwidmatches_new_does_not_panic_from_random_input(hwid: String) -> bool {
        let _ = HwidMatches::new(&hwid);
        true
    }

    // Corner cases which might not always represent what we want to/should parse. But they at
    // least illustrate how we are parsing device identification strings today.
    #[test]
    fn test_hwidmatches_new_corner_cases() {
        assert!(HwidMatches::new("").is_none());
        assert!(HwidMatches::new("ROOT").is_none());
        assert!(HwidMatches::new("ROOT\\").is_none());
        assert!(HwidMatches::new("USB\\").is_none());
        assert!(HwidMatches::new("USB\\VID_1234").is_none());
        assert!(HwidMatches::new("USB\\PID_1234").is_none());
        assert!(HwidMatches::new("USB\\MI_12").is_none());

        assert_eq!(
            HwidMatches::new("VID_1234&PID_5678").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: None,
                interface: None,
            }
        );

        assert_eq!(
            HwidMatches::new("ABC\\VID_1234&PID_5678&MI_90").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: None,
                interface: Some("90"),
            }
        );

        assert_eq!(
            HwidMatches::new("FTDIBUS\\VID_1234&PID_5678&MI_90").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: None,
                interface: Some("90"),
            }
        );

        assert_eq!(
            HwidMatches::new("USB\\VID_1234+PID_5678+MI_90").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: None,
                interface: Some("90"),
            }
        );

        assert_eq!(
            HwidMatches::new("FTDIBUS\\VID_1234+PID_5678\\0000").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: Some("0000"),
                interface: None,
            }
        );
    }

    #[test]
    fn test_hwidmatches_new_standard_cases_ftdi() {
        assert_eq!(
            HwidMatches::new("FTDIBUS\\VID_1234+PID_5678+SERIAL123\\0000").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: Some("SERIAL123"),
                interface: None,
            }
        );
    }

    #[test]
    fn test_hwidmatches_new_standard_cases_usb() {
        assert_eq!(
            HwidMatches::new("USB\\VID_1234&PID_5678").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: None,
                interface: None,
            }
        );

        assert_eq!(
            HwidMatches::new("USB\\VID_1234&PID_5678&MI_90").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: None,
                interface: Some("90"),
            }
        );

        assert_eq!(
            HwidMatches::new("USB\\VID_1234&PID_5678\\SERIAL123").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: Some("SERIAL123"),
                interface: None,
            }
        );

        assert_eq!(
            HwidMatches::new("USB\\VID_1234&PID_5678&MI_90\\SERIAL123").unwrap(),
            HwidMatches {
                vid: "1234",
                pid: "5678",
                serial: Some("SERIAL123"),
                interface: Some("90"),
            }
        );
    }

    #[test]
    fn test_parsing_usb_port_information() {
        let madeup_hwid = r"USB\VID_1D50&PID_6018+6&A694CA9&0&0000";
        let info = parse_usb_port_info(madeup_hwid, None).unwrap();
        // TODO: Fix returning no serial at all for devices without one. See issue #203.
        assert_eq!(info.serial_number, Some("6".to_string()));

        let bm_uart_hwid = r"USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0000";
        let bm_parent_hwid = r"USB\VID_1D50&PID_6018\85A12F01";
        let info = parse_usb_port_info(bm_uart_hwid, Some(bm_parent_hwid)).unwrap();

        assert_eq!(info.vid, 0x1D50);
        assert_eq!(info.pid, 0x6018);
        assert_eq!(info.serial_number, Some("85A12F01".to_string()));
        #[cfg(feature = "usbportinfo-interface")]
        assert_eq!(info.interface, Some(2));

        let ftdi_serial_hwid = r"FTDIBUS\VID_0403+PID_6001+A702TB52A\0000";
        let info = parse_usb_port_info(ftdi_serial_hwid, None).unwrap();

        assert_eq!(info.vid, 0x0403);
        assert_eq!(info.pid, 0x6001);
        assert_eq!(info.serial_number, Some("A702TB52A".to_string()));
        #[cfg(feature = "usbportinfo-interface")]
        assert_eq!(info.interface, None);

        let pyboard_hwid = r"USB\VID_F055&PID_9802\385435603432";
        let info = parse_usb_port_info(pyboard_hwid, None).unwrap();

        assert_eq!(info.vid, 0xF055);
        assert_eq!(info.pid, 0x9802);
        assert_eq!(info.serial_number, Some("385435603432".to_string()));
        #[cfg(feature = "usbportinfo-interface")]
        assert_eq!(info.interface, None);

        let unicode_serial = r"USB\VID_F055&PID_9802\3854356β03432&test";
        let info = parse_usb_port_info(unicode_serial, None).unwrap();
        assert_eq!(info.serial_number.as_deref(), Some("3854356β03432"));

        let unicode_serial = r"USB\VID_F055&PID_9802\3854356β03432";
        let info = parse_usb_port_info(unicode_serial, None).unwrap();
        assert_eq!(info.serial_number.as_deref(), Some("3854356β03432"));

        let unicode_serial = r"USB\VID_F055&PID_9802\3854356β";
        let info = parse_usb_port_info(unicode_serial, None).unwrap();
        assert_eq!(info.serial_number.as_deref(), Some("3854356β"));
    }
}
