//! IPFS `CIDv1` conversion helpers for `CoW` Protocol app-data.
//!
//! Every `CoW` Protocol order's `appData` hash can be mapped to an IPFS
//! Content Identifier (CID) so that the full JSON document is retrievable
//! from any IPFS gateway. This module handles the bidirectional conversion
//! between the 32-byte `appDataHex` stored on-chain and the `CIDv1` string
//! used by IPFS.
//!
//! The modern encoding uses `keccak256` with the `raw` multicodec (`0x55`).
//! Produced CIDs use multibase base16 lowercase (prefix `f`). Parsing also
//! accepts multibase base32 lowercase (prefix `b`, RFC 4648 unpadded), which
//! is the default multibase used by `multiformats`' `CID.parse` when no
//! explicit decoder is provided — so strings like
//! `bafkrei...` produced by the `TypeScript` SDK round-trip correctly.
//! Legacy helpers using `dag-pb` / `sha2-256` are preserved for backwards
//! compatibility but are deprecated.
//!
//! # Key functions
//!
//! | Function | Direction |
//! |---|---|
//! | [`appdata_hex_to_cid`] | `appDataHex` → `CIDv1` string |
//! | [`cid_to_appdata_hex`] | `CIDv1` string → `appDataHex` |
//! | [`parse_cid`] | `CIDv1` string → [`CidComponents`] |
//! | [`decode_cid`] | raw CID bytes → [`CidComponents`] |
//! | [`extract_digest`] | `CIDv1` string → digest hex |

use cow_errors::CowError;

// CIDv1 constants (modern encoding)
const CID_VERSION: u8 = 0x01;
const MULTICODEC_RAW: u8 = 0x55;
const HASH_KECCAK256: u8 = 0x1b;
const HASH_LEN: u8 = 0x20; // 32 bytes

// CIDv1 constants (legacy encoding: dag-pb + sha2-256)
const MULTICODEC_DAG_PB: u8 = 0x70;
const HASH_SHA2_256: u8 = 0x12;

/// Convert an `appDataHex` value (the 32-byte `keccak256` stored in the
/// order struct) into a `CIDv1` string.
///
/// The CID is built by hashing the raw bytes of `app_data_hex` with
/// `keccak256`, then wrapping the digest in a `CIDv1` envelope:
/// `[version=0x01, codec=0x55 (raw), hash_fn=0x1b (keccak256), len=0x20, ...digest]`.
/// The result is returned as a multibase base16 string (prefix `f`).
///
/// This is the inverse of [`cid_to_appdata_hex`].
///
/// Mirrors `appDataHexToCid` from the `@cowprotocol/app-data` `TypeScript`
/// package.
///
/// # Parameters
///
/// * `app_data_hex` — the `appData` value, with or without `0x` prefix.
///
/// # Returns
///
/// A base16 `CIDv1` string prefixed with `f` (e.g.
/// `f015501201b20...`).
///
/// # Errors
///
/// Returns [`CowError::AppData`] if `app_data_hex` is not valid hex.
///
/// # Example
///
/// ```
/// use cow_app_data::{appdata_hex_to_cid, cid_to_appdata_hex};
///
/// let hex = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
/// let cid = appdata_hex_to_cid(hex).unwrap();
/// assert!(cid.starts_with('f')); // multibase base16
/// ```
pub fn appdata_hex_to_cid(app_data_hex: &str) -> Result<String, CowError> {
    let hex = app_data_hex.strip_prefix("0x").map_or(app_data_hex, |s| s);
    let bytes = alloy_primitives::hex::decode(hex)
        .map_err(|e| CowError::AppData(format!("invalid hex: {e}")))?;

    if bytes.len() != HASH_LEN as usize {
        return Err(CowError::AppData(format!(
            "appDataHex must be {} bytes, got {}",
            HASH_LEN,
            bytes.len()
        )));
    }

    // The appDataHex is already the keccak256 hash of the canonical JSON
    // document, so it is used verbatim as the CID multihash digest. The
    // `HASH_KECCAK256` byte in the header declares the hash function that
    // produced that digest — re-hashing would break round-trips and diverge
    // from the TypeScript SDK's `appDataHexToCid`.
    let mut cid = Vec::with_capacity(4 + HASH_LEN as usize);
    cid.push(CID_VERSION);
    cid.push(MULTICODEC_RAW);
    cid.push(HASH_KECCAK256);
    cid.push(HASH_LEN);
    cid.extend_from_slice(&bytes);

    // Multibase base16 lowercase: prefix 'f'
    Ok(format!("f{}", alloy_primitives::hex::encode(&cid)))
}

/// Extract the digest from a `CIDv1` string and return it as
/// `0x`-prefixed hex.
///
/// This is the inverse of [`appdata_hex_to_cid`]: given a CID stored
/// alongside an order, recover the 32-byte digest embedded in the CID
/// header. The returned value can be used as the `appData` field in an
/// on-chain order struct.
///
/// Accepts multibase base16 (`f`/`F`) and base32 lowercase (`b`/`B`); other
/// multibase encodings return an error.
///
/// Mirrors `cidToAppDataHex` from the `@cowprotocol/app-data` `TypeScript`
/// package.
///
/// # Parameters
///
/// * `cid` — a multibase CID string (e.g. `"f015501201b20..."` or `"bafkrei..."`).
///
/// # Returns
///
/// A `0x`-prefixed, lowercase hex string of the 32-byte digest.
///
/// # Errors
///
/// Returns [`CowError::AppData`] if the multibase prefix is unsupported,
/// the payload is not valid, or the decoded bytes are shorter than 36
/// (4-byte header + 32-byte digest).
///
/// # Example
///
/// ```
/// use cow_app_data::{appdata_hex_to_cid, cid_to_appdata_hex};
///
/// let hex = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
/// let cid = appdata_hex_to_cid(hex).unwrap();
/// let recovered = cid_to_appdata_hex(&cid).unwrap();
/// assert!(recovered.starts_with("0x"));
/// assert_eq!(recovered.len(), 66); // "0x" + 64 hex chars
/// ```
pub fn cid_to_appdata_hex(cid: &str) -> Result<String, CowError> {
    let bytes = decode_multibase(cid)?;

    // Skip CIDv1 header: version(1) + codec(1) + hash_fn(1) + hash_len(1) = 4 bytes
    if bytes.len() < 4 + 32 {
        return Err(CowError::AppData("CID too short".into()));
    }
    let digest = &bytes[4..4 + 32];
    Ok(format!("0x{}", alloy_primitives::hex::encode(digest)))
}

/// Decode a multibase-prefixed CID string into raw bytes.
///
/// Supports the two prefixes emitted by the `multiformats` default base
/// registry for `CIDv1` payloads we care about:
///
/// - `f` / `F` → base16 lowercase (hex)
/// - `b` / `B` → base32 lowercase, RFC 4648, no padding
///
/// Returns [`CowError::AppData`] on unknown prefixes, invalid characters,
/// or empty input.
fn decode_multibase(cid: &str) -> Result<Vec<u8>, CowError> {
    let mut chars = cid.chars();
    let prefix = chars.next().ok_or_else(|| CowError::AppData("empty CID string".into()))?;
    let body = chars.as_str();

    match prefix {
        'f' | 'F' => alloy_primitives::hex::decode(body)
            .map_err(|e| CowError::AppData(format!("invalid CID hex: {e}"))),
        'b' | 'B' => decode_base32_lower_nopad(body),
        other => Err(CowError::AppData(format!(
            "unsupported CID multibase prefix '{other}' (expected 'f' or 'b')"
        ))),
    }
}

/// RFC 4648 base32 lowercase decoder, no padding (multibase `b`).
///
/// Alphabet: `abcdefghijklmnopqrstuvwxyz234567`. Uppercase input is also
/// accepted (callers should not rely on this — multibase reserves `B` for
/// the uppercase variant — but matching on a lowercased char is cheaper
/// than branching twice in [`decode_multibase`]).
fn decode_base32_lower_nopad(s: &str) -> Result<Vec<u8>, CowError> {
    let mut out = Vec::with_capacity(s.len() * 5 / 8);
    let mut buf: u32 = 0;
    let mut bits: u32 = 0;

    for c in s.chars() {
        let v: u32 = match c {
            'a'..='z' => (c as u32) - ('a' as u32),
            'A'..='Z' => (c as u32) - ('A' as u32),
            '2'..='7' => (c as u32) - ('2' as u32) + 26,
            _ => {
                return Err(CowError::AppData(format!("invalid base32 character '{c}'")));
            }
        };
        buf = (buf << 5) | v;
        bits += 5;
        if bits >= 8 {
            bits -= 8;
            out.push(((buf >> bits) & 0xff) as u8);
        }
    }

    // Trailing bits must be zero (canonical unpadded base32).
    if bits > 0 && (buf & ((1u32 << bits) - 1)) != 0 {
        return Err(CowError::AppData("non-canonical base32: trailing bits not zero".into()));
    }

    Ok(out)
}

// ── Legacy CID helpers ──────────────────────────────────────────────────────

/// Internal helper: build CID bytes from the given multicodec and hash
/// algorithm parameters.
///
/// This is the Rust equivalent of the `TypeScript` SDK's `_toCidBytes`.
fn to_cid_bytes(
    version: u8,
    multicodec: u8,
    hashing_algorithm: u8,
    hashing_length: u8,
    multihash_hex: &str,
) -> Result<Vec<u8>, CowError> {
    let hex = multihash_hex.strip_prefix("0x").map_or(multihash_hex, |s| s);
    let hash_bytes = alloy_primitives::hex::decode(hex)
        .map_err(|e| CowError::AppData(format!("invalid hex: {e}")))?;

    let mut cid = Vec::with_capacity(4 + hash_bytes.len());
    cid.push(version);
    cid.push(multicodec);
    cid.push(hashing_algorithm);
    cid.push(hashing_length);
    cid.extend_from_slice(&hash_bytes);
    Ok(cid)
}

/// Internal helper: convert an `appDataHex` to a `CIDv1` string using the
/// legacy encoding (`sha2-256` + `dag-pb` multicodec).
///
/// **Note**: Legacy CIDs used `CIDv0` (`base58btc`) in the `TypeScript` SDK. This Rust
/// implementation returns the CID as base16 (prefix `f`) since the crate does not
/// include a `base58` encoder. Callers requiring `CIDv0` format should convert externally.
///
/// This is the Rust equivalent of `_appDataHexToCidLegacy` in the `TypeScript` SDK.
fn app_data_hex_to_cid_legacy_aux(app_data_hex: &str) -> Result<String, CowError> {
    let cid_bytes =
        to_cid_bytes(CID_VERSION, MULTICODEC_DAG_PB, HASH_SHA2_256, HASH_LEN, app_data_hex)?;
    // Return as base16 since we don't have base58 encoding
    Ok(format!("f{}", alloy_primitives::hex::encode(&cid_bytes)))
}

/// Validate that a CID string is non-empty.
///
/// A simple guard used after CID derivation to ensure the conversion did
/// not silently produce an empty string. If `cid` is empty, returns an
/// error that includes the original `app_data_hex` for debugging.
///
/// Mirrors `_assertCid` from the `@cowprotocol/app-data` `TypeScript` package.
///
/// # Parameters
///
/// * `cid` — the CID string to validate.
/// * `app_data_hex` — the source hex, included in the error message on failure.
///
/// # Errors
///
/// Returns [`CowError::AppData`] if `cid` is empty.
pub fn assert_cid(cid: &str, app_data_hex: &str) -> Result<(), CowError> {
    if cid.is_empty() {
        return Err(CowError::AppData(format!("Error getting CID from appDataHex: {app_data_hex}")));
    }
    Ok(())
}

/// Convert an `appDataHex` to a `CIDv1` string using the legacy encoding.
///
/// Uses `dag-pb` multicodec with `sha2-256` hashing, matching the original
/// IPFS CID generation before `CoW` Protocol switched to `keccak256`.
///
/// **Note**: The `TypeScript` SDK returns a `CIDv0` (`base58btc`) string. This Rust
/// implementation returns base16 (prefix `f`) since no `base58` encoder is bundled.
///
/// # Errors
///
/// Returns [`CowError::AppData`] if `app_data_hex` cannot be decoded.
#[deprecated(
    note = "Use appdata_hex_to_cid instead — legacy CID encoding is no longer used by CoW Protocol"
)]
pub fn app_data_hex_to_cid_legacy(app_data_hex: &str) -> Result<String, CowError> {
    let cid = app_data_hex_to_cid_legacy_aux(app_data_hex)?;
    assert_cid(&cid, app_data_hex)?;
    Ok(cid)
}

/// Parsed components of an IPFS Content Identifier (CID).
///
/// A CID encodes four header fields followed by the raw hash digest:
///
/// ```text
/// ┌─────────┬───────┬──────────────┬────────────┬──────────────┐
/// │ version │ codec │ hash_function│ hash_length│   digest     │
/// │  (1 B)  │ (1 B) │    (1 B)     │   (1 B)    │ (N bytes)    │
/// └─────────┴───────┴──────────────┴────────────┴──────────────┘
/// ```
///
/// Use [`parse_cid`] to obtain this from a multibase string, or
/// [`decode_cid`] to obtain it from raw bytes.
///
/// # Example
///
/// ```
/// use cow_app_data::{appdata_hex_to_cid, parse_cid};
///
/// let hex = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
/// let cid = appdata_hex_to_cid(hex).unwrap();
/// let components = parse_cid(&cid).unwrap();
/// assert_eq!(components.version, 0x01); // CIDv1
/// assert_eq!(components.codec, 0x55); // raw multicodec
/// assert_eq!(components.hash_function, 0x1b); // keccak256
/// assert_eq!(components.hash_length, 0x20); // 32 bytes
/// assert_eq!(components.digest.len(), 32);
/// ```
#[derive(Debug, Clone)]
pub struct CidComponents {
    /// CID version (e.g. `1` for `CIDv1`).
    pub version: u8,
    /// Multicodec code (e.g. `0x55` for raw, `0x70` for dag-pb).
    pub codec: u8,
    /// Multihash function code (e.g. `0x1b` for keccak256, `0x12` for sha2-256).
    pub hash_function: u8,
    /// Hash digest length in bytes (typically `32`).
    pub hash_length: u8,
    /// The raw hash digest bytes.
    pub digest: Vec<u8>,
}

/// Parse a CID string into its constituent [`CidComponents`].
///
/// Decodes the multibase prefix, strips it, hex-decodes the remainder, and
/// splits the resulting bytes into the four header fields plus the digest.
///
/// Supports multibase base16 (`f`/`F`) and base32 lowercase (`b`/`B`, RFC
/// 4648 unpadded). Other multibase encodings (e.g. `base58btc` starting
/// with `Qm`) return an error.
///
/// Mirrors `parseCid` from the `@cowprotocol/app-data` `TypeScript` package.
///
/// # Parameters
///
/// * `ipfs_hash` — a multibase-encoded CID string (e.g. `"f015501201b20..."` or `"bafkrei..."`).
///
/// # Returns
///
/// A [`CidComponents`] struct with the parsed version, codec, hash function,
/// hash length, and raw digest bytes.
///
/// # Errors
///
/// Returns [`CowError::AppData`] if the multibase prefix is unsupported,
/// the body is malformed, or the decoded payload is shorter than 4 bytes.
///
/// # Example
///
/// ```
/// use cow_app_data::{appdata_hex_to_cid, parse_cid};
///
/// let hex = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
/// let cid = appdata_hex_to_cid(hex).unwrap();
/// let c = parse_cid(&cid).unwrap();
/// assert_eq!(c.version, 1);
/// assert_eq!(c.digest.len(), 32);
/// ```
pub fn parse_cid(ipfs_hash: &str) -> Result<CidComponents, CowError> {
    let bytes = decode_multibase(ipfs_hash)?;

    if bytes.len() < 4 {
        return Err(CowError::AppData("CID too short".into()));
    }

    let version = bytes[0];
    let codec = bytes[1];
    let hash_function = bytes[2];
    let hash_length = bytes[3];
    let digest = bytes[4..].to_vec();

    Ok(CidComponents { version, codec, hash_function, hash_length, digest })
}

/// Decode raw CID bytes into their constituent [`CidComponents`].
///
/// Unlike [`parse_cid`], this function operates on raw bytes rather than a
/// multibase-encoded string. Use it when you already have the CID as a byte
/// slice (e.g. from a binary protocol or a database column).
///
/// Mirrors `decodeCid` from the `@cowprotocol/app-data` `TypeScript` package.
///
/// # Parameters
///
/// * `bytes` — raw CID bytes: `[version, codec, hash_fn, hash_len, ...digest]`.
///
/// # Returns
///
/// A [`CidComponents`] struct with the parsed fields.
///
/// # Errors
///
/// Returns [`CowError::AppData`] if the byte slice is shorter than 4 bytes
/// (the minimum CID header size).
///
/// # Example
///
/// ```
/// use cow_app_data::decode_cid;
///
/// let mut bytes = vec![0x01, 0x55, 0x1b, 0x20];
/// bytes.extend_from_slice(&[0u8; 32]); // 32 digest bytes
/// let c = decode_cid(&bytes).unwrap();
/// assert_eq!(c.version, 1);
/// assert_eq!(c.codec, 0x55);
/// assert_eq!(c.digest.len(), 32);
/// ```
pub fn decode_cid(bytes: &[u8]) -> Result<CidComponents, CowError> {
    if bytes.len() < 4 {
        return Err(CowError::AppData("CID bytes too short".into()));
    }

    Ok(CidComponents {
        version: bytes[0],
        codec: bytes[1],
        hash_function: bytes[2],
        hash_length: bytes[3],
        digest: bytes[4..].to_vec(),
    })
}

/// Extract the multihash digest from a CID string and return it as
/// `0x`-prefixed hex.
///
/// Parses the CID via [`parse_cid`], then returns only the raw digest
/// portion as a `0x`-prefixed hex string. This is useful when you have a
/// CID from IPFS and need to recover the hash digest to match against
/// on-chain `appData` values.
///
/// Note: the digest extracted here is the hash **inside** the CID, not the
/// original `appDataHex`. For round-trip conversion use [`cid_to_appdata_hex`].
///
/// Mirrors `extractDigest` from the `@cowprotocol/app-data` `TypeScript`
/// package.
///
/// # Parameters
///
/// * `cid` — a base16 multibase CID string.
///
/// # Returns
///
/// A `0x`-prefixed hex string of the raw digest bytes.
///
/// # Errors
///
/// Returns [`CowError::AppData`] if the CID cannot be parsed.
///
/// # Example
///
/// ```
/// use cow_app_data::{appdata_hex_to_cid, extract_digest};
///
/// let hex = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
/// let cid = appdata_hex_to_cid(hex).unwrap();
/// let digest = extract_digest(&cid).unwrap();
/// assert!(digest.starts_with("0x"));
/// assert_eq!(digest.len(), 66); // "0x" + 64 hex chars
/// ```
pub fn extract_digest(cid: &str) -> Result<String, CowError> {
    let components = parse_cid(cid)?;
    Ok(format!("0x{}", alloy_primitives::hex::encode(&components.digest)))
}

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

    const SAMPLE_HEX: &str = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";

    #[test]
    fn appdata_hex_to_cid_produces_base16_cid() {
        let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
        assert!(cid.starts_with('f'));
        // CID header (4 bytes) + digest (32 bytes) = 36 bytes → 72 hex chars + 'f' prefix
        assert_eq!(cid.len(), 1 + 72);
    }

    #[test]
    fn appdata_hex_to_cid_without_0x_prefix() {
        let hex = SAMPLE_HEX.strip_prefix("0x").unwrap_or_else(|| SAMPLE_HEX);
        let cid = appdata_hex_to_cid(hex).unwrap_or_default();
        assert!(cid.starts_with('f'));
    }

    #[test]
    fn cid_to_appdata_hex_roundtrip() {
        let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap();
        let recovered = cid_to_appdata_hex(&cid).unwrap();
        assert!(recovered.starts_with("0x"));
        assert_eq!(recovered.len(), 66);
        assert_eq!(recovered, SAMPLE_HEX);
    }

    #[test]
    fn appdata_hex_to_cid_uses_input_as_digest() {
        // The appDataHex is already a keccak256; it must become the CID digest
        // verbatim (no extra hashing), matching the TypeScript SDK.
        let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap();
        let components = parse_cid(&cid).unwrap();
        let expected = alloy_primitives::hex::decode(SAMPLE_HEX.trim_start_matches("0x")).unwrap();
        assert_eq!(components.digest, expected);
    }

    #[test]
    fn appdata_hex_to_cid_rejects_wrong_length() {
        assert!(appdata_hex_to_cid("0xdeadbeef").is_err());
    }

    #[test]
    fn cid_to_appdata_hex_rejects_unsupported_multibase() {
        // base58btc (prefix 'Q') is not supported.
        assert!(cid_to_appdata_hex("Qmabc123").is_err());
        // 'z' multibase is not supported.
        assert!(cid_to_appdata_hex("zabc123").is_err());
    }

    #[test]
    fn cid_to_appdata_hex_rejects_empty() {
        assert!(cid_to_appdata_hex("").is_err());
    }

    #[test]
    fn cid_to_appdata_hex_decodes_uppercase_base32() {
        // Multibase 'B' (uppercase base32) must decode identically to 'b'.
        let cid = "BAFKREIEAQHRRDSFTXWXASEAYMNEMP7F7V6PY6PSBH7NQCNPM5RQHMESVXI";
        let expected = "0x8081e311c8b3bdae0910186348c7fcbfaf9f8f3e413fdb0135ecec60761255ba";
        assert_eq!(cid_to_appdata_hex(cid).unwrap(), expected);
    }

    #[test]
    fn cid_to_appdata_hex_rejects_invalid_base32_char() {
        // '1' is not in the RFC 4648 base32 alphabet.
        let err = cid_to_appdata_hex("b1xyz").unwrap_err();
        assert!(format!("{err}").contains("invalid base32 character"));
    }

    #[test]
    fn cid_to_appdata_hex_rejects_non_canonical_base32() {
        // "az" yields 1 decoded byte with 2 trailing bits equal to 0b01;
        // the decoder must reject this as non-canonical rather than silently
        // truncating.
        let err = cid_to_appdata_hex("baz").unwrap_err();
        assert!(format!("{err}").contains("non-canonical base32"));
    }

    #[test]
    fn cid_to_appdata_hex_decodes_base32_vector() {
        // Parity vector produced by @cowprotocol/app-data's `cidToAppDataHex`.
        // Multibase base32 lowercase (prefix 'b'), RFC 4648 unpadded — the
        // default encoding emitted by `multiformats`' `CID.toString()` for
        // CIDv1 when no explicit base is chosen.
        let cid = "bafkreieaqhrrdsftxwxaseaymnemp7f7v6py6psbh7nqcnpm5rqhmesvxi";
        let expected = "0x8081e311c8b3bdae0910186348c7fcbfaf9f8f3e413fdb0135ecec60761255ba";
        assert_eq!(cid_to_appdata_hex(cid).unwrap(), expected);
    }

    #[test]
    fn cid_to_appdata_hex_rejects_too_short() {
        assert!(cid_to_appdata_hex("f0155").is_err());
    }

    #[test]
    fn parse_cid_components() {
        let cid = appdata_hex_to_cid(SAMPLE_HEX).expect("SAMPLE_HEX is a valid 32-byte digest");
        let c = parse_cid(&cid).expect("round-tripped CID is valid");
        assert_eq!(c.version, CID_VERSION);
        assert_eq!(c.codec, MULTICODEC_RAW);
        assert_eq!(c.hash_function, HASH_KECCAK256);
        assert_eq!(c.hash_length, HASH_LEN);
        assert_eq!(c.digest.len(), 32);
    }

    #[test]
    fn parse_cid_rejects_unsupported_multibase() {
        // '_' is not a multibase prefix we support.
        assert!(parse_cid("_not_a_cid").is_err());
        // base58btc CIDv0 (leading 'Q') is rejected.
        assert!(parse_cid("QmSomething").is_err());
    }

    #[test]
    fn parse_cid_base32_components() {
        // This vector comes from the TypeScript SDK parity suite. It is a
        // `raw` CIDv1 but hashed with sha2-256 (0x12), not keccak, so we
        // assert the actual header the decoder must produce rather than the
        // keccak-specific constants used elsewhere in the module.
        let cid = "bafkreieaqhrrdsftxwxaseaymnemp7f7v6py6psbh7nqcnpm5rqhmesvxi";
        let c = parse_cid(cid).unwrap();
        assert_eq!(c.version, CID_VERSION);
        assert_eq!(c.codec, MULTICODEC_RAW);
        assert_eq!(c.hash_function, HASH_SHA2_256);
        assert_eq!(c.hash_length, HASH_LEN);
        assert_eq!(c.digest.len(), 32);
    }

    #[test]
    fn parse_cid_rejects_too_short() {
        assert!(parse_cid("f01").is_err());
    }

    #[test]
    fn decode_cid_from_bytes() {
        let mut bytes = vec![0x01, 0x55, 0x1b, 0x20];
        bytes.extend_from_slice(&[0xaa; 32]);
        let c = decode_cid(&bytes).expect("hand-crafted CID bytes are valid");
        assert_eq!(c.version, 1);
        assert_eq!(c.codec, 0x55);
        assert_eq!(c.digest.len(), 32);
    }

    #[test]
    fn decode_cid_rejects_short_bytes() {
        assert!(decode_cid(&[0x01, 0x02, 0x03]).is_err());
        assert!(decode_cid(&[]).is_err());
    }

    #[test]
    fn extract_digest_returns_0x_prefixed() {
        let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
        let digest = extract_digest(&cid).unwrap_or_default();
        assert!(digest.starts_with("0x"));
        assert_eq!(digest.len(), 66);
    }

    #[test]
    fn assert_cid_accepts_nonempty() {
        assert!(assert_cid("f01234", "0xabc").is_ok());
    }

    #[test]
    fn assert_cid_rejects_empty() {
        assert!(assert_cid("", "0xabc").is_err());
    }

    #[test]
    #[allow(deprecated, reason = "testing legacy API surface")]
    fn legacy_cid_produces_base16_string() {
        let cid = app_data_hex_to_cid_legacy(SAMPLE_HEX).unwrap_or_default();
        assert!(cid.starts_with('f'));
    }

    #[test]
    fn appdata_hex_to_cid_invalid_hex() {
        assert!(appdata_hex_to_cid("0xZZZZ").is_err());
    }

    #[test]
    fn deterministic_output() {
        let cid1 = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
        let cid2 = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
        assert_eq!(cid1, cid2);
    }

    #[test]
    fn cid_to_appdata_hex_invalid_hex() {
        assert!(cid_to_appdata_hex("fZZZZinvalid").is_err());
    }

    #[test]
    fn parse_cid_uppercase_f_prefix() {
        let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap();
        // Replace lowercase 'f' prefix with uppercase 'F'
        let upper = format!("F{}", &cid[1..]);
        let c = parse_cid(&upper).unwrap();
        assert_eq!(c.version, CID_VERSION);
    }

    #[test]
    fn to_cid_bytes_without_0x() {
        let hex = SAMPLE_HEX.strip_prefix("0x").unwrap();
        let bytes = to_cid_bytes(CID_VERSION, MULTICODEC_RAW, HASH_KECCAK256, HASH_LEN, hex);
        assert!(bytes.is_ok());
    }

    #[test]
    fn to_cid_bytes_invalid_hex() {
        let result = to_cid_bytes(CID_VERSION, MULTICODEC_RAW, HASH_KECCAK256, HASH_LEN, "ZZZZ");
        assert!(result.is_err());
    }
}
