use crate::error::HostLinkError;
use std::fmt;

const DEVICE_TYPES_PARSE_ORDER: &[&str] = &[
    "MR", "LR", "CR", "VB", "DM", "EM", "FM", "ZF", "TM", "TC", "TS", "CC", "CS", "AT", "CM", "VM",
    "R", "B", "W", "Z", "T", "C", "X", "Y", "M", "L", "D", "E", "F",
];
const FORCE_DEVICE_TYPES: &[&str] = &["R", "B", "MR", "LR", "CR", "T", "C", "VB"];
const MBS_DEVICE_TYPES: &[&str] = &[
    "R", "B", "MR", "LR", "CR", "T", "C", "VB", "X", "Y", "M", "L",
];
const MWS_DEVICE_TYPES: &[&str] = &[
    "R", "B", "MR", "LR", "CR", "VB", "X", "Y", "DM", "EM", "FM", "W", "TM", "Z", "TC", "TS", "CC",
    "CS", "CM", "VM",
];
const RDC_DEVICE_TYPES: &[&str] = &[
    "R", "B", "MR", "LR", "CR", "DM", "EM", "FM", "ZF", "W", "TM", "Z", "T", "C", "CM", "X", "Y",
    "M", "L", "D", "E", "F",
];
const WR_DEVICE_TYPES: &[&str] = &[
    "R", "B", "MR", "LR", "CR", "VB", "DM", "EM", "FM", "ZF", "W", "TM", "Z", "T", "TC", "TS", "C",
    "CC", "CS", "CM", "VM", "X", "Y", "M", "L", "D", "E", "F",
];
const WS_DEVICE_TYPES: &[&str] = &["T", "C"];

#[derive(Debug, Clone, Copy)]
struct DeviceRange {
    lo: u32,
    hi: u32,
    base: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KvDeviceAddress {
    pub device_type: String,
    pub number: u32,
    pub suffix: String,
}

impl KvDeviceAddress {
    pub fn to_text(&self) -> Result<String, HostLinkError> {
        let range = device_range(&self.device_type).ok_or_else(|| {
            HostLinkError::protocol(format!("Unsupported device type: {}", self.device_type))
        })?;
        let number = if uses_bit_bank_address(&self.device_type) {
            format_bit_bank_number(self.number)
        } else if uses_xym_bit_address(&self.device_type) {
            format_xym_bit_number(self.number)
        } else if range.base == 16 {
            format!("{:X}", self.number)
        } else {
            self.number.to_string()
        };
        Ok(format!("{}{}{}", self.device_type, number, self.suffix))
    }
}

impl fmt::Display for KvDeviceAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.to_text() {
            Ok(text) => write!(f, "{text}"),
            Err(_) => write!(f, "{}{}{}", self.device_type, self.number, self.suffix),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KvLogicalAddress {
    pub base_address: KvDeviceAddress,
    pub data_type: String,
    pub bit_index: Option<u8>,
}

impl KvLogicalAddress {
    pub fn is_bit_in_word(&self) -> bool {
        self.bit_index.is_some()
    }

    pub fn to_text(&self) -> Result<String, HostLinkError> {
        let mut base = self.base_address.clone();
        base.suffix.clear();
        let base_text = base.to_text()?;
        if let Some(bit_index) = self.bit_index {
            return Ok(format!("{base_text}.{bit_index:X}"));
        }

        if self.data_type == logical_default_dtype_by_device_type(&self.base_address.device_type) {
            Ok(base_text)
        } else {
            Ok(format!("{base_text}:{}", self.data_type))
        }
    }
}

pub struct HostLinkAddress;

impl HostLinkAddress {
    pub fn parse(text: &str) -> Result<KvDeviceAddress, HostLinkError> {
        parse_device(text)
    }

    pub fn try_parse(text: &str) -> Option<KvDeviceAddress> {
        parse_device(text).ok()
    }

    pub fn format(address: &KvDeviceAddress) -> Result<String, HostLinkError> {
        address.to_text()
    }

    pub fn normalize(text: &str) -> Result<String, HostLinkError> {
        if let Ok(address) = parse_device(text) {
            return address.to_text();
        }

        parse_logical_address(text)?.to_text()
    }

    pub fn parse_logical(text: &str) -> Result<KvLogicalAddress, HostLinkError> {
        parse_logical_address(text)
    }

    pub fn try_parse_logical(text: &str) -> Option<KvLogicalAddress> {
        parse_logical_address(text).ok()
    }

    pub fn normalize_logical(text: &str) -> Result<String, HostLinkError> {
        parse_logical_address(text)?.to_text()
    }
}

pub(crate) fn model_name_for_code(code: &str) -> &str {
    match code {
        "134" => "KV-N24nn",
        "133" => "KV-N40nn",
        "132" => "KV-N60nn",
        "128" => "KV-NC32T",
        "63" => "KV-X550",
        "61" => "KV-X530",
        "60" => "KV-X520",
        "62" => "KV-X500",
        "59" => "KV-X310",
        "58" => "KV-8000A",
        "57" => "KV-8000",
        "55" => "KV-7500",
        "54" => "KV-7300",
        "53" => "KV-5500",
        "52" => "KV-5000",
        "51" => "KV-3000",
        "50" => "KV-1000",
        "49" => "KV-700 (With expansion memory)",
        "48" => "KV-700 (No expansion memory)",
        _ => "Unknown",
    }
}

pub(crate) fn force_device_types() -> &'static [&'static str] {
    FORCE_DEVICE_TYPES
}

pub(crate) fn mbs_device_types() -> &'static [&'static str] {
    MBS_DEVICE_TYPES
}

pub(crate) fn mws_device_types() -> &'static [&'static str] {
    MWS_DEVICE_TYPES
}

pub(crate) fn rdc_device_types() -> &'static [&'static str] {
    RDC_DEVICE_TYPES
}

pub(crate) fn wr_device_types() -> &'static [&'static str] {
    WR_DEVICE_TYPES
}

pub(crate) fn ws_device_types() -> &'static [&'static str] {
    WS_DEVICE_TYPES
}

pub(crate) fn default_format_by_device_type(device_type: &str) -> &'static str {
    match device_type {
        "R" | "B" | "MR" | "LR" | "CR" | "VB" | "X" | "Y" | "M" | "L" => "",
        "DM" | "EM" | "FM" | "ZF" | "W" | "TM" | "Z" | "CM" | "VM" | "D" | "E" | "F" => ".U",
        "AT" => ".D",
        "T" | "TC" | "TS" | "C" | "CC" | "CS" => ".D",
        _ => "",
    }
}

pub(crate) fn logical_default_dtype_by_device_type(device_type: &str) -> &'static str {
    default_format_by_device_type(device_type).trim_start_matches('.')
}

pub(crate) fn is_direct_bit_device_type(device_type: &str) -> bool {
    matches!(
        device_type,
        "R" | "B" | "MR" | "LR" | "CR" | "VB" | "X" | "Y" | "M" | "L"
    )
}

pub(crate) fn uses_bit_bank_address(device_type: &str) -> bool {
    matches!(device_type, "R" | "MR" | "LR" | "CR")
}

fn uses_xym_bit_address(device_type: &str) -> bool {
    matches!(device_type, "X" | "Y")
}

fn is_valid_bit_bank_number(number: u32) -> bool {
    number % 100 <= 15
}

pub(crate) fn bit_bank_logical_number(number: u32) -> u32 {
    (number / 100) * 16 + (number % 100)
}

fn bit_bank_number_from_logical(number: u32) -> u32 {
    (number / 16) * 100 + (number % 16)
}

fn format_bit_bank_number(number: u32) -> String {
    let bank = number / 100;
    let bit = number % 100;
    format!("{bank}{bit:02}")
}

fn format_xym_bit_number(number: u32) -> String {
    let bank = number / 16;
    let bit = number % 16;
    format!("{bank}{bit:X}")
}

pub(crate) fn is_optimizable_read_named_device_type(device_type: &str) -> bool {
    default_format_by_device_type(device_type) == ".U"
}

pub(crate) fn offset_device(
    start: &KvDeviceAddress,
    word_offset: u32,
) -> Result<String, HostLinkError> {
    let mut next = start.clone();
    next.number = if uses_bit_bank_address(&next.device_type) {
        let logical = bit_bank_logical_number(next.number)
            .checked_add(word_offset)
            .ok_or_else(|| HostLinkError::protocol("Device offset overflow"))?;
        bit_bank_number_from_logical(logical)
    } else {
        next.number
            .checked_add(word_offset)
            .ok_or_else(|| HostLinkError::protocol("Device offset overflow"))?
    };
    next.suffix.clear();
    next.to_text()
}

pub(crate) fn parse_named_address_parts(
    address: &str,
) -> Result<(String, String, Option<u8>), HostLinkError> {
    let logical = parse_logical_address(address)?;
    let mut base = logical.base_address;
    base.suffix.clear();
    Ok((base.to_text()?, logical.data_type, logical.bit_index))
}

pub fn normalize_suffix(suffix: impl AsRef<str>) -> Result<String, HostLinkError> {
    let suffix = suffix.as_ref();
    if suffix.is_empty() {
        return Ok(String::new());
    }

    let mut normalized = suffix.trim().to_ascii_uppercase();
    if !normalized.starts_with('.') {
        normalized.insert(0, '.');
    }

    match normalized.as_str() {
        ".U" | ".S" | ".D" | ".L" | ".H" => Ok(normalized),
        _ => Err(HostLinkError::protocol(format!(
            "Unsupported data format suffix: {suffix}"
        ))),
    }
}

pub fn parse_device(text: &str) -> Result<KvDeviceAddress, HostLinkError> {
    parse_device_internal(text, true)
}

fn parse_device_internal(
    text: &str,
    allow_omitted_type: bool,
) -> Result<KvDeviceAddress, HostLinkError> {
    let raw = text.trim().to_ascii_uppercase();
    if raw.is_empty() {
        return Err(HostLinkError::protocol("Device string must not be empty"));
    }

    let (core, suffix) = extract_suffix(&raw)?;
    let (device_type, number_text) = if let Some(device_type) = DEVICE_TYPES_PARSE_ORDER
        .iter()
        .find(|candidate| core.starts_with(**candidate))
    {
        (
            (*device_type).to_owned(),
            core[device_type.len()..].to_owned(),
        )
    } else if allow_omitted_type && core.bytes().all(|byte| byte.is_ascii_digit()) {
        ("R".to_owned(), core.to_owned())
    } else {
        return Err(HostLinkError::protocol(format!(
            "Invalid device string '{text}'. Valid device types: {}.",
            DEVICE_TYPES_PARSE_ORDER.join(", ")
        )));
    };

    if number_text.is_empty() || !number_text.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(HostLinkError::protocol(format!(
            "Invalid device number for {device_type}: {number_text}"
        )));
    }

    let range = device_range(&device_type).ok_or_else(|| {
        HostLinkError::protocol(format!("Unsupported device type: {device_type}"))
    })?;

    let number = if uses_xym_bit_address(&device_type) {
        parse_xym_bit_number(&device_type, &number_text)?
    } else {
        u32::from_str_radix(&number_text, range.base).map_err(|_| {
            HostLinkError::protocol(format!(
                "Invalid device number for {device_type}: {number_text}"
            ))
        })?
    };
    if number < range.lo || number > range.hi {
        return Err(HostLinkError::protocol(format!(
            "Device number out of range: {device_type}{number_text} (allowed: {}..{})",
            format_device_number(&device_type, range.lo),
            format_device_number(&device_type, range.hi)
        )));
    }
    if uses_bit_bank_address(&device_type) && !is_valid_bit_bank_number(number) {
        return Err(HostLinkError::protocol(format!(
            "Invalid bit-bank device number: {device_type}{number_text} (lower two digits must be 00..15)"
        )));
    }

    Ok(KvDeviceAddress {
        device_type,
        number,
        suffix,
    })
}

pub fn parse_logical_address(text: &str) -> Result<KvLogicalAddress, HostLinkError> {
    let raw = text.trim();
    if raw.is_empty() {
        return Err(HostLinkError::protocol("Address must not be empty"));
    }

    if let Some(colon_index) = raw.find(':') {
        let base = parse_device(&raw[..colon_index])?;
        let mut base = base;
        base.suffix.clear();
        return Ok(KvLogicalAddress {
            base_address: base,
            data_type: normalize_dtype(&raw[colon_index + 1..])?,
            bit_index: None,
        });
    }

    if let Some(dot_index) = raw.rfind('.') {
        if let Ok(bit_index) = u8::from_str_radix(&raw[dot_index + 1..], 16) {
            if bit_index <= 15 {
                let mut base = parse_device(&raw[..dot_index])?;
                base.suffix.clear();
                return Ok(KvLogicalAddress {
                    base_address: base,
                    data_type: "BIT_IN_WORD".to_owned(),
                    bit_index: Some(bit_index),
                });
            }
        }
    }

    let mut base = parse_device(raw)?;
    let data_type = if base.suffix.is_empty() {
        logical_default_dtype_by_device_type(&base.device_type).to_owned()
    } else {
        normalize_dtype(&base.suffix)?
    };
    base.suffix.clear();
    Ok(KvLogicalAddress {
        base_address: base,
        data_type,
        bit_index: None,
    })
}

pub fn resolve_effective_format(device_type: &str, suffix: &str) -> String {
    if suffix.is_empty() {
        default_format_by_device_type(device_type).to_owned()
    } else {
        suffix.to_owned()
    }
}

pub fn validate_device_type(
    command: &str,
    device_type: &str,
    allowed_types: &[&str],
) -> Result<(), HostLinkError> {
    if allowed_types.contains(&device_type) {
        Ok(())
    } else {
        Err(HostLinkError::protocol(format!(
            "Command '{command}' does not support device type '{device_type}'. Supported types: {}.",
            allowed_types.join(", ")
        )))
    }
}

pub fn validate_device_count(
    device_type: &str,
    effective_format: &str,
    count: usize,
) -> Result<(), HostLinkError> {
    let is_32_bit = matches!(effective_format, ".D" | ".L");
    let (lo, hi) = match device_type {
        "TM" => (1, if is_32_bit { 256 } else { 512 }),
        "Z" => (1, 12),
        "AT" => (1, 8),
        "T" | "TC" | "TS" | "C" | "CC" | "CS" => (1, 120),
        _ => (1, if is_32_bit { 500 } else { 1000 }),
    };

    if !(lo..=hi).contains(&count) {
        return Err(HostLinkError::protocol(format!(
            "Count {count} is out of range for device type '{device_type}' with format '{effective_format}' (allowed: {lo}..{hi})."
        )));
    }

    Ok(())
}

pub fn validate_device_span(
    device_type: &str,
    start_number: u32,
    effective_format: &str,
    count: usize,
) -> Result<(), HostLinkError> {
    let range = device_range(device_type).ok_or_else(|| {
        HostLinkError::protocol(format!("Unsupported device type: {device_type}"))
    })?;
    if count == 0 {
        return Err(HostLinkError::protocol(
            "count out of range: 0 (allowed: 1..)",
        ));
    }

    let word_width = if device_type == "AT" {
        1u32
    } else if matches!(effective_format, ".D" | ".L") {
        2u32
    } else {
        1u32
    };
    let start_span_number = if uses_bit_bank_address(device_type) {
        bit_bank_logical_number(start_number)
    } else {
        start_number
    };
    let hi_span_number = if uses_bit_bank_address(device_type) {
        bit_bank_logical_number(range.hi)
    } else {
        range.hi
    };
    let end_span_number = start_span_number
        .checked_add((count as u32).saturating_mul(word_width))
        .and_then(|value| value.checked_sub(1))
        .ok_or_else(|| HostLinkError::protocol("Device span overflow"))?;

    if start_number < range.lo || start_number > range.hi || end_span_number > hi_span_number {
        let start_text = format_device_number(device_type, start_number);
        let end_number = if uses_bit_bank_address(device_type) {
            bit_bank_number_from_logical(end_span_number)
        } else {
            end_span_number
        };
        let end_text = format_device_number(device_type, end_number);
        return Err(HostLinkError::protocol(format!(
            "Device span out of range: {device_type}{start_text}..{device_type}{end_text} with format '{effective_format}'"
        )));
    }

    Ok(())
}

pub fn validate_expansion_buffer_count(
    effective_format: &str,
    count: usize,
) -> Result<(), HostLinkError> {
    let hi = if matches!(effective_format, ".D" | ".L") {
        500
    } else {
        1000
    };
    if !(1..=hi).contains(&count) {
        return Err(HostLinkError::protocol(format!(
            "Count {count} is out of range for expansion buffer format '{effective_format}' (allowed: 1..{hi})."
        )));
    }
    Ok(())
}

pub fn validate_expansion_buffer_span(
    address: u32,
    effective_format: &str,
    count: usize,
) -> Result<(), HostLinkError> {
    if count == 0 {
        return Err(HostLinkError::protocol(
            "count out of range: 0 (allowed: 1..)",
        ));
    }

    let word_width = if matches!(effective_format, ".D" | ".L") {
        2u32
    } else {
        1u32
    };
    let end_address = address
        .checked_add((count as u32).saturating_mul(word_width))
        .and_then(|value| value.checked_sub(1))
        .ok_or_else(|| HostLinkError::protocol("Expansion buffer span overflow"))?;
    if address > 59_999 || end_address > 59_999 {
        return Err(HostLinkError::protocol(format!(
            "Expansion buffer span out of range: {address}..{end_address} with format '{effective_format}'"
        )));
    }
    Ok(())
}

fn normalize_dtype(text: &str) -> Result<String, HostLinkError> {
    match text
        .trim()
        .trim_start_matches('.')
        .to_ascii_uppercase()
        .as_str()
    {
        "U" => Ok("U".to_owned()),
        "S" => Ok("S".to_owned()),
        "D" => Ok("D".to_owned()),
        "L" => Ok("L".to_owned()),
        "F" => Ok("F".to_owned()),
        "COMMENT" => Ok("COMMENT".to_owned()),
        _ => Err(HostLinkError::protocol(format!(
            "Unsupported logical data type '{text}'."
        ))),
    }
}

fn extract_suffix(raw: &str) -> Result<(&str, String), HostLinkError> {
    if raw.len() >= 2 && raw.as_bytes()[raw.len() - 2] == b'.' {
        let suffix = normalize_suffix(&raw[raw.len() - 2..])?;
        Ok((&raw[..raw.len() - 2], suffix))
    } else {
        Ok((raw, String::new()))
    }
}

fn format_device_number(device_type: &str, value: u32) -> String {
    if uses_bit_bank_address(device_type) {
        return format_bit_bank_number(value);
    }
    if uses_xym_bit_address(device_type) {
        return format_xym_bit_number(value);
    }

    let Some(range) = device_range(device_type) else {
        return value.to_string();
    };
    if range.base == 16 {
        format!("{value:X}")
    } else {
        value.to_string()
    }
}

fn parse_xym_bit_number(device_type: &str, number_text: &str) -> Result<u32, HostLinkError> {
    let bank_text = if number_text.len() == 1 {
        "0"
    } else {
        &number_text[..number_text.len() - 1]
    };
    if !bank_text.bytes().all(|byte| byte.is_ascii_digit()) {
        return Err(HostLinkError::protocol(format!(
            "Invalid X/Y device number: {device_type}{number_text} (bank digits must be decimal and bit digit must be 0..F)"
        )));
    }

    let bank = bank_text.parse::<u32>().map_err(|_| {
        HostLinkError::protocol(format!(
            "Invalid device number for {device_type}: {number_text}"
        ))
    })?;
    let bit = u32::from_str_radix(&number_text[number_text.len() - 1..], 16).map_err(|_| {
        HostLinkError::protocol(format!(
            "Invalid device number for {device_type}: {number_text}"
        ))
    })?;
    bank.checked_mul(16)
        .and_then(|value| value.checked_add(bit))
        .ok_or_else(|| {
            HostLinkError::protocol(format!(
                "Invalid device number for {device_type}: {number_text}"
            ))
        })
}

fn device_range(device_type: &str) -> Option<DeviceRange> {
    let range = match device_type {
        "R" => DeviceRange {
            lo: 0,
            hi: 199_915,
            base: 10,
        },
        "B" => DeviceRange {
            lo: 0,
            hi: 0x7FFF,
            base: 16,
        },
        "MR" => DeviceRange {
            lo: 0,
            hi: 399_915,
            base: 10,
        },
        "LR" => DeviceRange {
            lo: 0,
            hi: 99_915,
            base: 10,
        },
        "CR" => DeviceRange {
            lo: 0,
            hi: 7_915,
            base: 10,
        },
        "VB" => DeviceRange {
            lo: 0,
            hi: 0xF9FF,
            base: 16,
        },
        "DM" => DeviceRange {
            lo: 0,
            hi: 65_534,
            base: 10,
        },
        "EM" => DeviceRange {
            lo: 0,
            hi: 65_534,
            base: 10,
        },
        "FM" => DeviceRange {
            lo: 0,
            hi: 32_767,
            base: 10,
        },
        "ZF" => DeviceRange {
            lo: 0,
            hi: 524_287,
            base: 10,
        },
        "W" => DeviceRange {
            lo: 0,
            hi: 0x7FFF,
            base: 16,
        },
        "TM" => DeviceRange {
            lo: 0,
            hi: 511,
            base: 10,
        },
        "Z" => DeviceRange {
            lo: 1,
            hi: 12,
            base: 10,
        },
        "T" | "TC" | "TS" | "C" | "CC" | "CS" => DeviceRange {
            lo: 0,
            hi: 3_999,
            base: 10,
        },
        "AT" => DeviceRange {
            lo: 0,
            hi: 7,
            base: 10,
        },
        "CM" => DeviceRange {
            lo: 0,
            hi: 7_599,
            base: 10,
        },
        "VM" => DeviceRange {
            lo: 0,
            hi: 589_823,
            base: 10,
        },
        "X" => DeviceRange {
            lo: 0,
            hi: 1_999 * 16 + 15,
            base: 10,
        },
        "Y" => DeviceRange {
            lo: 0,
            hi: 1_999 * 16 + 15,
            base: 10,
        },
        "M" => DeviceRange {
            lo: 0,
            hi: 63_999,
            base: 10,
        },
        "L" => DeviceRange {
            lo: 0,
            hi: 15_999,
            base: 10,
        },
        "D" | "E" => DeviceRange {
            lo: 0,
            hi: 65_534,
            base: 10,
        },
        "F" => DeviceRange {
            lo: 0,
            hi: 32_767,
            base: 10,
        },
        _ => return None,
    };
    Some(range)
}

#[cfg(test)]
mod tests {
    use super::{
        HostLinkAddress, offset_device, parse_device, parse_logical_address, validate_device_span,
        wr_device_types,
    };

    #[test]
    fn parse_device_normalizes_hex_suffix_and_number() {
        let address = parse_device("w1a.h").unwrap();
        assert_eq!(address.device_type, "W");
        assert_eq!(address.number, 0x1A);
        assert_eq!(address.suffix, ".H");
        assert_eq!(address.to_text().unwrap(), "W1A.H");
    }

    #[test]
    fn parse_logical_bit_index_uses_hex_notation() {
        let logical = parse_logical_address("dm100.a").unwrap();
        assert_eq!(logical.to_text().unwrap(), "DM100.A");
        assert_eq!(logical.bit_index, Some(10));
    }

    #[test]
    fn normalize_plain_address_keeps_default_r_omission_rule() {
        assert_eq!(HostLinkAddress::normalize("100").unwrap(), "R100");
    }

    #[test]
    fn parse_logical_comment_address_round_trips() {
        let logical = parse_logical_address("dm100:comment").unwrap();
        assert_eq!(logical.to_text().unwrap(), "DM100:COMMENT");
        assert_eq!(logical.data_type, "COMMENT");
    }

    #[test]
    fn parse_logical_direct_bit_defaults_to_bool_read() {
        let logical = parse_logical_address("cr0").unwrap();
        assert_eq!(logical.to_text().unwrap(), "CR000");
        assert_eq!(logical.data_type, "");
    }

    #[test]
    fn parse_device_rejects_invalid_bit_bank_numbers() {
        assert!(parse_device("R016").is_err());
        assert!(parse_device("MR116").is_err());
        assert!(parse_device("LR99916").is_err());
        assert!(parse_device("CR7916").is_err());
    }

    #[test]
    fn parse_device_accepts_valid_bit_bank_boundaries() {
        assert_eq!(parse_device("R0").unwrap().to_text().unwrap(), "R000");
        assert_eq!(parse_device("R1").unwrap().to_text().unwrap(), "R001");
        assert_eq!(parse_device("R015").unwrap().to_text().unwrap(), "R015");
        assert_eq!(parse_device("R100").unwrap().to_text().unwrap(), "R100");
        assert_eq!(parse_device("MR115").unwrap().to_text().unwrap(), "MR115");
        assert_eq!(parse_device("CR0").unwrap().to_text().unwrap(), "CR000");
        assert_eq!(parse_device("CR7915").unwrap().to_text().unwrap(), "CR7915");
    }

    #[test]
    fn bit_bank_offsets_cross_bank_boundaries_by_bit_position() {
        let start = parse_device("CR3614").unwrap();
        assert_eq!(offset_device(&start, 0).unwrap(), "CR3614");
        assert_eq!(offset_device(&start, 1).unwrap(), "CR3615");
        assert_eq!(offset_device(&start, 2).unwrap(), "CR3700");
        assert_eq!(offset_device(&start, 18).unwrap(), "CR3800");
    }

    #[test]
    fn validate_device_span_uses_bit_bank_point_count() {
        validate_device_span("CR", 7900, "", 16).unwrap();
        assert!(validate_device_span("CR", 7900, "", 17).is_err());
    }

    #[test]
    fn validate_device_span_treats_at_32bit_as_device_points() {
        validate_device_span("AT", 7, ".D", 1).unwrap();
        validate_device_span("AT", 0, ".D", 8).unwrap();
        assert!(validate_device_span("AT", 1, ".D", 8).is_err());
    }

    #[test]
    fn parse_device_accepts_high_xym_m_addresses() {
        assert_eq!(parse_device("M63872").unwrap().to_text().unwrap(), "M63872");
        assert!(parse_device("M64000").is_err());
    }

    #[test]
    fn parse_device_uses_decimal_bank_hex_bit_for_xym_bits() {
        let address = parse_device("X390").unwrap();
        assert_eq!(address.device_type, "X");
        assert_eq!(address.number, 39 * 16);
        assert_eq!(address.to_text().unwrap(), "X390");

        assert_eq!(
            parse_device("X3F0").unwrap_err().to_string(),
            "Invalid X/Y device number: X3F0 (bank digits must be decimal and bit digit must be 0..F)"
        );
        assert_eq!(parse_device("X1999F").unwrap().to_text().unwrap(), "X1999F");
        assert!(parse_device("X20000").is_err());
        assert_eq!(parse_device("Y1999F").unwrap().to_text().unwrap(), "Y1999F");
        assert!(parse_device("Y20000").is_err());
    }

    #[test]
    fn validate_device_span_allows_xym_m_upper_bound() {
        validate_device_span("X", 1_999 * 16 + 15, "", 1).unwrap();
        assert!(validate_device_span("X", 1_999 * 16 + 15, "", 2).is_err());
        validate_device_span("Y", 1_999 * 16 + 15, "", 1).unwrap();
        assert!(validate_device_span("Y", 1_999 * 16 + 15, "", 2).is_err());
        validate_device_span("M", 63_998, "", 1).unwrap();
        validate_device_span("M", 63_998, "", 2).unwrap();
        assert!(validate_device_span("M", 63_999, "", 2).is_err());
        assert!(validate_device_span("L", 16_000, "", 1).is_err());
    }

    #[test]
    fn parse_logical_suffix_preserves_explicit_type() {
        let logical = parse_logical_address("dm100.s").unwrap();
        assert_eq!(logical.to_text().unwrap(), "DM100:S");
        assert_eq!(logical.data_type, "S");
    }

    #[test]
    fn parse_logical_counter_defaults_to_dword_read() {
        let logical = parse_logical_address("t0").unwrap();
        assert_eq!(logical.to_text().unwrap(), "T0");
        assert_eq!(logical.data_type, "D");
    }

    #[test]
    fn parse_logical_at_defaults_to_dword_read() {
        let logical = parse_logical_address("at7").unwrap();
        assert_eq!(logical.to_text().unwrap(), "AT7");
        assert_eq!(logical.data_type, "D");
    }

    #[test]
    fn wr_device_types_exclude_at() {
        assert!(!wr_device_types().contains(&"AT"));
        assert!(wr_device_types().contains(&"DM"));
        assert!(wr_device_types().contains(&"TS"));
    }
}
