//! NTAG 424 DNA wrapped command builders, parsers, and NXP status words.
//!
//! All commands use CLA=0x90 NXP wrapping. Pure functions — no transport dependency.
//! Wire these with `NfcConnection::transmit()` from `nfc.rs` for live tag interaction.

/// NXP proprietary status word (SW1=0x91, SW2=status).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NxpStatus {
    Success,
    AdditionalFrame,
    IntegrityError,
    LengthError,
    PermissionDenied,
    ParameterError,
    AuthenticationError,
    BoundaryError,
    CommandAborted,
    MemoryError,
    FileNotFound,
    NoSuchKey,
    Unknown(u8),
}

impl NxpStatus {
    pub fn from_sw2(sw2: u8) -> Self {
        match sw2 {
            0x00 => Self::Success,
            0xAF => Self::AdditionalFrame,
            0x1E => Self::IntegrityError,
            0x7E => Self::LengthError,
            0x9D => Self::PermissionDenied,
            0x9E => Self::ParameterError,
            0xAE => Self::AuthenticationError,
            0xBE => Self::BoundaryError,
            0xCA => Self::CommandAborted,
            0xEE => Self::MemoryError,
            0xF0 => Self::FileNotFound,
            0x40 => Self::NoSuchKey,
            other => Self::Unknown(other),
        }
    }

    pub fn to_sw2(self) -> u8 {
        match self {
            Self::Success => 0x00,
            Self::AdditionalFrame => 0xAF,
            Self::IntegrityError => 0x1E,
            Self::LengthError => 0x7E,
            Self::PermissionDenied => 0x9D,
            Self::ParameterError => 0x9E,
            Self::AuthenticationError => 0xAE,
            Self::BoundaryError => 0xBE,
            Self::CommandAborted => 0xCA,
            Self::MemoryError => 0xEE,
            Self::FileNotFound => 0xF0,
            Self::NoSuchKey => 0x40,
            Self::Unknown(v) => v,
        }
    }

    pub fn is_success(self) -> bool {
        self == Self::Success
    }

    pub fn description(self) -> &'static str {
        match self {
            Self::Success => "success",
            Self::AdditionalFrame => "additional frame expected",
            Self::IntegrityError => "integrity error",
            Self::LengthError => "length error",
            Self::PermissionDenied => "permission denied",
            Self::ParameterError => "parameter error",
            Self::AuthenticationError => "authentication error",
            Self::BoundaryError => "boundary error",
            Self::CommandAborted => "command aborted",
            Self::MemoryError => "memory error",
            Self::FileNotFound => "file not found",
            Self::NoSuchKey => "no such key",
            Self::Unknown(_) => "unknown status",
        }
    }
}

impl std::fmt::Display for NxpStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "91{:02X} ({})", self.to_sw2(), self.description())
    }
}

/// Parsed response from an NXP wrapped command.
#[derive(Debug, Clone)]
pub struct NxpResponse {
    pub status: NxpStatus,
    pub data: Vec<u8>,
}

/// Parse a raw APDU response into NXP status + payload.
/// Returns `None` if the response is not an NXP 91xx response.
pub fn parse_nxp_response(raw: &[u8]) -> Option<NxpResponse> {
    if raw.len() < 2 {
        return None;
    }
    let sw1 = raw[raw.len() - 2];
    let sw2 = raw[raw.len() - 1];
    if sw1 != 0x91 {
        return None;
    }
    Some(NxpResponse {
        status: NxpStatus::from_sw2(sw2),
        data: raw[..raw.len() - 2].to_vec(),
    })
}

/// Expect a successful NXP response (91 00) and return the payload.
pub fn expect_success(raw: &[u8]) -> Result<Vec<u8>, String> {
    let resp = parse_nxp_response(raw)
        .ok_or_else(|| format!("not an NXP response: {}", hex::encode(raw)))?;
    if !resp.status.is_success() {
        return Err(format!("NXP command failed: {}", resp.status));
    }
    Ok(resp.data)
}

/// Expect 91AF (additional frame) and return the partial payload.
pub fn expect_additional_frame(raw: &[u8]) -> Result<Vec<u8>, String> {
    let resp = parse_nxp_response(raw)
        .ok_or_else(|| format!("not an NXP response: {}", hex::encode(raw)))?;
    if resp.status != NxpStatus::AdditionalFrame {
        return Err(format!("expected 91AF, got: {}", resp.status));
    }
    Ok(resp.data)
}

// ---------------------------------------------------------------------------
// Wrapped command builders (CLA=0x90)
// ---------------------------------------------------------------------------

/// Build NXP wrapped APDU: 90 [cmd] 00 00 [Lc] [data...] 00
fn wrap_command(cmd: u8, data: &[u8]) -> Vec<u8> {
    let mut apdu = Vec::with_capacity(6 + data.len());
    apdu.push(0x90); // CLA
    apdu.push(cmd);  // INS
    apdu.push(0x00); // P1
    apdu.push(0x00); // P2
    if !data.is_empty() {
        apdu.push(data.len() as u8); // Lc
        apdu.extend_from_slice(data);
    }
    apdu.push(0x00); // Le
    apdu
}

/// GetVersion (60h) — first frame of three-frame exchange.
pub fn cmd_get_version() -> Vec<u8> {
    wrap_command(0x60, &[])
}

/// AdditionalFrame (AFh) — continuation for multi-frame exchanges.
pub fn cmd_additional_frame() -> Vec<u8> {
    wrap_command(0xAF, &[])
}

/// AuthenticateEV2First part 1 (71h) — send key number and LenCap.
pub fn cmd_auth_ev2_first_part1(key_no: u8) -> Vec<u8> {
    wrap_command(0x71, &[key_no, 0x00])
}

/// AuthenticateEV2First part 2 (AFh) — send encrypted challenge response.
pub fn cmd_auth_ev2_first_part2(encrypted_data: &[u8]) -> Vec<u8> {
    wrap_command(0xAF, encrypted_data)
}

/// GetFileSettings (F5h) for a given file number.
pub fn cmd_get_file_settings(file_no: u8) -> Vec<u8> {
    wrap_command(0xF5, &[file_no])
}

/// ReadData (ADh) — plain mode, no authentication.
/// Offset and length are 3-byte little-endian each.
pub fn cmd_read_data_plain(file_no: u8, offset: u32, length: u32) -> Vec<u8> {
    let mut data = vec![file_no];
    data.extend_from_slice(&(offset as u32).to_le_bytes()[..3]);
    data.extend_from_slice(&(length as u32).to_le_bytes()[..3]);
    wrap_command(0xAD, &data)
}

/// WriteData (8Dh) — plain mode, no authentication.
/// Offset is 3-byte little-endian, followed by length (3-byte LE) and payload.
pub fn cmd_write_data_plain(file_no: u8, offset: u32, payload: &[u8]) -> Vec<u8> {
    let len = payload.len() as u32;
    let mut data = vec![file_no];
    data.extend_from_slice(&offset.to_le_bytes()[..3]);
    data.extend_from_slice(&len.to_le_bytes()[..3]);
    data.extend_from_slice(payload);
    wrap_command(0x8D, &data)
}

/// ChangeFileSettings (5Fh) — requires authenticated session.
/// `settings_data` is the raw settings payload (encrypted+MACed by caller in auth mode).
pub fn cmd_change_file_settings(file_no: u8, settings_data: &[u8]) -> Vec<u8> {
    let mut data = vec![file_no];
    data.extend_from_slice(settings_data);
    wrap_command(0x5F, &data)
}

// ---------------------------------------------------------------------------
// Response parsers
// ---------------------------------------------------------------------------

/// Parsed GetVersion response (three frames concatenated).
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct GetVersionInfo {
    /// Hardware info (frame 1, 7 bytes).
    pub hw_vendor: u8,
    pub hw_type: u8,
    pub hw_subtype: u8,
    pub hw_major: u8,
    pub hw_minor: u8,
    pub hw_storage_size: u8,
    pub hw_protocol: u8,
    /// Software info (frame 2, 7 bytes).
    pub sw_vendor: u8,
    pub sw_type: u8,
    pub sw_subtype: u8,
    pub sw_major: u8,
    pub sw_minor: u8,
    pub sw_storage_size: u8,
    pub sw_protocol: u8,
    /// Production info (frame 3, 14 bytes).
    pub uid: [u8; 7],
    pub batch_no: [u8; 5],
    pub production_week: u8,
    pub production_year: u8,
}

impl GetVersionInfo {
    /// True if this looks like an NTAG 424 DNA (NXP, Type 0x04).
    /// Subtype 0x02 = standard NTAG 424 DNA, 0x05 = NTAG 424 DNA TagTamper.
    pub fn is_ntag424_dna(&self) -> bool {
        self.hw_vendor == 0x04 && self.hw_type == 0x04 && (self.hw_subtype == 0x02 || self.hw_subtype == 0x05)
    }

    pub fn uid_hex(&self) -> String {
        self.uid.iter().map(|b| format!("{b:02X}")).collect()
    }
}

/// Parse concatenated GetVersion data (frame1 + frame2 + frame3 = 7+7+14 = 28 bytes).
pub fn parse_get_version(data: &[u8]) -> Result<GetVersionInfo, String> {
    if data.len() < 28 {
        return Err(format!(
            "GetVersion data too short: {} bytes (need 28)",
            data.len()
        ));
    }
    let mut uid = [0u8; 7];
    uid.copy_from_slice(&data[14..21]);
    let mut batch_no = [0u8; 5];
    batch_no.copy_from_slice(&data[21..26]);

    Ok(GetVersionInfo {
        hw_vendor: data[0],
        hw_type: data[1],
        hw_subtype: data[2],
        hw_major: data[3],
        hw_minor: data[4],
        hw_storage_size: data[5],
        hw_protocol: data[6],
        sw_vendor: data[7],
        sw_type: data[8],
        sw_subtype: data[9],
        sw_major: data[10],
        sw_minor: data[11],
        sw_storage_size: data[12],
        sw_protocol: data[13],
        uid,
        batch_no,
        production_week: data[26],
        production_year: data[27],
    })
}

/// Parsed file settings from GetFileSettings response.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FileSettings {
    pub file_type: u8,
    pub comm_mode: CommMode,
    pub sdm_enabled: bool,
    pub access_rights: u16,
    pub file_size: u32,
    pub sdm_options: Option<SdmOptions>,
}

/// Communication mode for file access.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommMode {
    Plain,
    Mac,
    Full,
}

/// SDM-related options parsed from extended file settings.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SdmOptions {
    pub sdm_option_byte: u8,
    pub sdm_access_rights: Vec<u8>,
    pub raw_tail: Vec<u8>,
}

/// Parse GetFileSettings response payload.
pub fn parse_file_settings(data: &[u8]) -> Result<FileSettings, String> {
    if data.len() < 7 {
        return Err(format!(
            "FileSettings data too short: {} bytes (need >= 7)",
            data.len()
        ));
    }
    let file_type = data[0];
    let file_option = data[1];
    let comm_bits = file_option & 0x03;
    let comm_mode = match comm_bits {
        0x00 => CommMode::Plain,
        0x01 => CommMode::Mac,
        0x03 => CommMode::Full,
        _ => CommMode::Plain,
    };
    let sdm_enabled = file_option & 0x40 != 0;
    let access_rights = u16::from_be_bytes([data[2], data[3]]);
    let file_size =
        u32::from(data[4]) | (u32::from(data[5]) << 8) | (u32::from(data[6]) << 16);

    let sdm_options = if sdm_enabled && data.len() > 7 {
        let sdm_option_byte = data[7];
        let tail = &data[8..];
        Some(SdmOptions {
            sdm_option_byte,
            sdm_access_rights: if tail.len() >= 2 {
                tail[..2].to_vec()
            } else {
                Vec::new()
            },
            raw_tail: tail.to_vec(),
        })
    } else {
        None
    };

    Ok(FileSettings {
        file_type,
        comm_mode,
        sdm_enabled,
        access_rights,
        file_size,
        sdm_options,
    })
}

// ---------------------------------------------------------------------------
// File settings builder for ChangeFileSettings
// ---------------------------------------------------------------------------

/// Access rights nibble values. 0x0E = free, 0x0F = denied, 0x00..0x04 = key number.
pub const ACCESS_FREE: u8 = 0x0E;
pub const ACCESS_DENIED: u8 = 0x0F;

/// Declarative NDEF file access policy. Never accept raw hex as primary API.
#[derive(Debug, Clone)]
pub struct NdefAccessPolicy {
    /// Public read (phone tap). Use true for SDM workflow.
    pub ndef_read_free: bool,
    /// Key required for NDEF write (0..4). Must allow key 0 during enrollment.
    pub ndef_write_key: u8,
    /// Key required for read-write. Typically denied (0xF) or key 0.
    pub ndef_read_write_key: u8,
    /// Key required for ChangeFileSettings. Must be key 0 for enrollment.
    pub change_settings_key: u8,
}

impl Default for NdefAccessPolicy {
    fn default() -> Self {
        Self {
            ndef_read_free: true,
            ndef_write_key: 0,   // key 0 required — we write NDEF during enrollment
            ndef_read_write_key: 0x0F, // denied
            change_settings_key: 0,   // key 0 required
        }
    }
}

impl NdefAccessPolicy {
    /// Serialize to the 2-byte access rights word.
    /// NXP AN12196 nibble order: ReadWrite | Change | Read | Write (MSB to LSB).
    pub fn to_access_rights(&self) -> u16 {
        let read = if self.ndef_read_free { ACCESS_FREE } else { ACCESS_DENIED };
        let write = self.ndef_write_key.min(4);
        let rw = self.ndef_read_write_key.min(0x0F);
        let change = self.change_settings_key.min(4);
        build_access_rights(read, write, rw, change)
    }
}

/// Build access rights word. NXP AN12196 nibble order: ReadWrite<<12 | Change<<8 | Read<<4 | Write.
pub fn build_access_rights(read: u8, write: u8, read_write: u8, change: u8) -> u16 {
    ((read_write as u16 & 0x0F) << 12)
        | ((change as u16 & 0x0F) << 8)
        | ((read as u16 & 0x0F) << 4)
        | (write as u16 & 0x0F)
}

/// Decode access rights. NXP order: ReadWrite, Change, Read, Write.
pub fn decode_access_rights(ar: u16) -> [String; 4] {
    let read_write = (ar >> 12) as u8 & 0x0F;
    let change = (ar >> 8) as u8 & 0x0F;
    let read = (ar >> 4) as u8 & 0x0F;
    let write = ar as u8 & 0x0F;
    [
        format!("read_write={}", nibble_to_role(read_write)),
        format!("change={}", nibble_to_role(change)),
        format!("read={}", nibble_to_role(read)),
        format!("write={}", nibble_to_role(write)),
    ]
}

fn nibble_to_role(n: u8) -> String {
    match n {
        0x0E => "free".into(),
        0x0F => "denied".into(),
        k if k <= 4 => format!("key{k}"),
        _ => format!("0x{n:01X}"),
    }
}

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

    #[test]
    fn default_policy_access_rights() {
        let policy = NdefAccessPolicy::default();
        let ar = policy.to_access_rights();
        // NXP order: ReadWrite|Change|Read|Write. Default: read=free, write=key0, rw=denied, change=key0
        assert_eq!(ar, 0xF0E0, "ReadWrite=0xF Change=0 Read=0xE Write=0");
        let decoded = decode_access_rights(ar);
        assert!(decoded[2].contains("free")); // read
        assert!(decoded[3].contains("key0")); // write
        assert!(decoded[0].contains("denied")); // read_write
        assert!(decoded[1].contains("key0")); // change
    }

    #[test]
    fn nxp_example_access_rights_00e0() {
        // NXP 00E0 = ReadWrite=0, Change=0, Read=E, Write=0
        let ar = build_access_rights(0x0E, 0, 0, 0);
        assert_eq!(ar, 0x00E0);
    }

    #[test]
    fn decode_nxp_f121() {
        // SDMAccessRights F121 = RFU=F, SDMCtrRet=1, SDMMetaRead=2, SDMFileRead=1
        let ar = 0xF121u16;
        let b0 = (ar >> 8) as u8;
        let b1 = ar as u8;
        assert_eq!(b0, 0xF1, "RFU|CtrRet");
        assert_eq!(b1, 0x21, "MetaRead|FileRead");
    }
}

/// Configuration for SDM-enabled file settings.
#[derive(Debug, Clone)]
pub struct SdmConfig {
    /// FileOption byte: CommMode bits + SDM enable bit (0x40).
    pub comm_mode: CommMode,
    /// Access rights for read/write/readwrite/change.
    pub access_rights: u16,
    /// SDM options byte (UID mirror, ReadCtr mirror, ReadCtrLimit, EncFileData, etc.).
    pub sdm_options: u8,
    /// SDM access rights (2 bytes: MetaReadKey | FileReadKey | CtrRetKey).
    pub sdm_meta_read_key: u8,
    pub sdm_file_read_key: u8,
    pub sdm_ctr_ret_key: u8,
    /// Offset fields (3 bytes each, little-endian).
    pub uid_offset: Option<u32>,
    pub read_ctr_offset: Option<u32>,
    pub picc_data_offset: Option<u32>,
    pub mac_input_offset: Option<u32>,
    pub enc_offset: Option<u32>,
    pub enc_length: Option<u32>,
    pub mac_offset: Option<u32>,
    pub read_ctr_limit: Option<u32>,
}

/// Exact NXP AN12196 ChangeFileSettings CmdData example for serializer verification.
/// 4000E0C1F121200000430000430000 — proves wire format before custom offsets.
pub fn nxp_example_cmd_data() -> Vec<u8> {
    hex::decode("4000E0C1F121200000430000430000").expect("NXP example hex")
}

/// Build the ChangeFileSettings payload for SDM configuration.
/// This is the plaintext payload; caller must encrypt+MAC for authenticated session.
pub fn build_sdm_file_settings(cfg: &SdmConfig) -> Vec<u8> {
    let comm_bits = match cfg.comm_mode {
        CommMode::Plain => 0x00,
        CommMode::Mac => 0x01,
        CommMode::Full => 0x03,
    };
    let file_option = comm_bits | 0x40; // SDM enabled

    let mut out = Vec::with_capacity(32);
    out.push(file_option);
    out.extend_from_slice(&cfg.access_rights.to_be_bytes());
    out.push(cfg.sdm_options);

    // SDM access rights. NXP AN12196 byte order: [RFU|SDMCtrRet][SDMMetaRead|SDMFileRead].
    // F121h = RFU=F, SDMCtrRet=1, SDMMetaRead=2, SDMFileRead=1.
    let sdm_ar_byte0 = (0x0F << 4) | (cfg.sdm_ctr_ret_key & 0x0F); // RFU=F, SDMCtrRet
    let sdm_ar_byte1 = (cfg.sdm_meta_read_key << 4) | (cfg.sdm_file_read_key & 0x0F); // SDMMetaRead, SDMFileRead
    out.push(sdm_ar_byte0);
    out.push(sdm_ar_byte1);

    let push_offset = |v: &mut Vec<u8>, off: Option<u32>| {
        if let Some(o) = off {
            v.extend_from_slice(&o.to_le_bytes()[..3]);
        }
    };

    if cfg.sdm_options & 0x80 != 0 {
        // UID mirror enabled
        push_offset(&mut out, cfg.uid_offset);
    }
    if cfg.sdm_options & 0x40 != 0 {
        // ReadCtr mirror enabled
        push_offset(&mut out, cfg.read_ctr_offset);
    }
    if cfg.sdm_options & 0x20 != 0 {
        // ReadCtrLimit enabled
        if let Some(limit) = cfg.read_ctr_limit {
            out.extend_from_slice(&limit.to_le_bytes()[..3]);
        }
    }
    // Offsets per NXP datasheet Table 69 order:
    // PICCDataOffset, SDMMACInputOffset, [SDMENCOffset, SDMENCLength], SDMMACOffset.
    // AN12196 example had both MAC offsets = 67, masking this order dependency.
    push_offset(&mut out, cfg.picc_data_offset);
    push_offset(&mut out, cfg.mac_input_offset);
    if cfg.sdm_options & 0x08 != 0 {
        push_offset(&mut out, cfg.enc_offset);
        push_offset(&mut out, cfg.enc_length);
    }
    push_offset(&mut out, cfg.mac_offset);

    out
}

// ---------------------------------------------------------------------------
// CRC32 for ChangeKey (ISO 3309 / CCITT, same as DESFire EV2)
// ---------------------------------------------------------------------------

const CRC32_TABLE: [u32; 256] = {
    let mut table = [0u32; 256];
    let mut i = 0;
    while i < 256 {
        let mut crc = i as u32;
        let mut j = 0;
        while j < 8 {
            if crc & 1 != 0 {
                crc = (crc >> 1) ^ 0xEDB88320;
            } else {
                crc >>= 1;
            }
            j += 1;
        }
        table[i] = crc;
        i += 1;
    }
    table
};

/// Compute CRC32 (ISO 3309) used by DESFire EV2 / NTAG 424 DNA ChangeKey.
pub fn crc32(data: &[u8]) -> u32 {
    let mut crc: u32 = 0xFFFF_FFFF;
    for &b in data {
        let idx = ((crc ^ u32::from(b)) & 0xFF) as usize;
        crc = (crc >> 8) ^ CRC32_TABLE[idx];
    }
    crc
}

/// Prepare ChangeKey plaintext for changing a key different from the auth key.
/// Result must be encrypted before sending.
/// Per AN12196 Table 26: KeyData = Old⊕New || Version || CRC32(NewKey) || Padding.
/// `key_version`: new key version to store on chip (0–255).
pub fn build_change_key_data_other(
    new_key: &[u8; 16],
    old_key: &[u8; 16],
    _key_no: u8,
    key_version: u8,
) -> Vec<u8> {
    let mut xored = [0u8; 16];
    for i in 0..16 {
        xored[i] = new_key[i] ^ old_key[i];
    }
    let crc_new = crc32(new_key);

    let mut plaintext = Vec::with_capacity(21);
    plaintext.extend_from_slice(&xored);
    plaintext.push(key_version);
    plaintext.extend_from_slice(&crc_new.to_le_bytes());
    // encrypt_command adds ISO 9797-1 M2 padding (0x80 || 0x00...)
    plaintext
}

/// Prepare ChangeKey plaintext for changing the currently authenticated key.
/// Result must be encrypted before sending.
/// Per AN12196 Table 27: Plaintext = NewKey || KeyVer || Padding (no CRC for Case 2).
/// `key_version`: new key version to store on chip (0–255).
pub fn build_change_key_data_self(new_key: &[u8; 16], _key_no: u8, key_version: u8) -> Vec<u8> {
    let mut plaintext = Vec::with_capacity(17);
    plaintext.extend_from_slice(new_key);
    plaintext.push(key_version);
    // encrypt_command adds ISO 9797-1 M2 padding (0x80 || 0x00...)
    plaintext
}

// ---------------------------------------------------------------------------
// Regression tests — lock known-good serialization (enrollment plan #2)
// ---------------------------------------------------------------------------

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

    /// NXP AN12196 exact ChangeFileSettings CmdData. Regression: must not change.
    const NXP_EXAMPLE_HEX: &str = "4000E0C1F121200000430000430000";

    #[test]
    fn nxp_example_cmd_data_matches_an12196() {
        let expected = hex::decode(NXP_EXAMPLE_HEX).expect("NXP example hex");
        let actual = nxp_example_cmd_data();
        assert_eq!(actual, expected, "nxp_example_cmd_data must match NXP AN12196");
    }

    /// GetFileSettings(02) response from a personalized tag (known-good baseline).
    /// Format: file_type(1) + file_option(1) + access_rights(2) + file_size(3) + sdm_options...
    const BASELINE_FILE_SETTINGS_HEX: &str =
        "004000e0000100c1f121200000430000430000";

    #[test]
    fn parse_baseline_file_settings_response() {
        let data = hex::decode(BASELINE_FILE_SETTINGS_HEX).expect("baseline hex");
        let fs = parse_file_settings(&data).expect("parse FileSettings");
        assert_eq!(fs.file_type, 0x00);
        assert_eq!(fs.comm_mode, CommMode::Plain);
        assert!(fs.sdm_enabled);
        assert_eq!(fs.access_rights, 0x00E0);
        assert_eq!(fs.file_size, 256);
        assert!(fs.sdm_options.is_some());
    }

    /// build_sdm_file_settings with exact NXP AN12196 config must produce nxp_example bytes.
    /// NXP example: F121 = SDMCtrRet=1, SDMMetaRead=2, SDMFileRead=1; offsets 32, 67, 67.
    #[test]
    fn build_sdm_file_settings_matches_nxp_example() {
        let cfg = SdmConfig {
            comm_mode: CommMode::Plain,
            access_rights: 0x00E0,
            sdm_options: 0xC1,
            sdm_meta_read_key: 0x02,
            sdm_file_read_key: 0x01,
            sdm_ctr_ret_key: 0x01,
            uid_offset: None,
            read_ctr_offset: None,
            picc_data_offset: Some(32),
            mac_input_offset: Some(67),
            enc_offset: None,
            enc_length: None,
            mac_offset: Some(67),
            read_ctr_limit: None,
        };
        let built = build_sdm_file_settings(&cfg);
        let expected = nxp_example_cmd_data();
        assert_eq!(built, expected, "build_sdm_file_settings(NXP config) must match nxp_example_cmd_data");
    }
}

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

    #[test]
    fn status_round_trip() {
        for sw2 in [0x00, 0xAF, 0x1E, 0x7E, 0x9D, 0x9E, 0xAE, 0xBE, 0xCA, 0xEE, 0xF0, 0x40] {
            let status = NxpStatus::from_sw2(sw2);
            assert_eq!(status.to_sw2(), sw2);
        }
    }

    #[test]
    fn parse_response_91xx() {
        let raw = [0x01, 0x02, 0x03, 0x91, 0x00];
        let resp = parse_nxp_response(&raw).unwrap();
        assert!(resp.status.is_success());
        assert_eq!(resp.data, vec![0x01, 0x02, 0x03]);
    }

    #[test]
    fn parse_response_non_nxp() {
        let raw = [0x90, 0x00];
        assert!(parse_nxp_response(&raw).is_none());
    }

    #[test]
    fn wrap_command_no_data() {
        let apdu = cmd_get_version();
        assert_eq!(apdu, vec![0x90, 0x60, 0x00, 0x00, 0x00]);
    }

    #[test]
    fn wrap_command_with_data() {
        let apdu = cmd_get_file_settings(0x02);
        assert_eq!(apdu, vec![0x90, 0xF5, 0x00, 0x00, 0x01, 0x02, 0x00]);
    }

    #[test]
    fn file_settings_delivery_state() {
        // Typical delivery state: StandardData, Plain, no SDM, 256 bytes
        let data = [0x00, 0x00, 0x00, 0xE0, 0x00, 0x01, 0x00];
        let fs = parse_file_settings(&data).unwrap();
        assert_eq!(fs.file_type, 0x00);
        assert_eq!(fs.comm_mode, CommMode::Plain);
        assert!(!fs.sdm_enabled);
        assert_eq!(fs.file_size, 256);
    }

    #[test]
    fn crc32_known_value() {
        // NXP DESFire EV2 uses JAMCRC (CRC-32 without final XOR inversion).
        // JAMCRC("123456789") = 0x340BC6D9 (complementary to ISO 3309's 0xCBF43926).
        let data = b"123456789";
        assert_eq!(crc32(data), 0x340BC6D9);
    }
}
