//! High-performance Rust implementation of ISO 24138:2024 (ISCC).
//!
//! This crate provides the core ISCC algorithm implementations. All 10 `gen_*_v0`
//! functions are the public Tier 1 API surface, designed to be compatible with
//! the `iscc-core` Python reference implementation.

pub(crate) mod cdc;
pub mod codec;
pub(crate) mod conformance;
pub(crate) mod dct;
pub(crate) mod minhash;
pub(crate) mod simhash;
pub mod streaming;
pub mod types;
pub(crate) mod utils;
pub(crate) mod wtahash;

pub use cdc::alg_cdc_chunks;
pub use codec::encode_base64;
pub use codec::iscc_decompose;
pub use conformance::conformance_selftest;
pub use minhash::alg_minhash_256;
pub use simhash::{alg_simhash, sliding_window};
pub use streaming::{DataHasher, InstanceHasher};
pub use types::*;
#[cfg(feature = "text-processing")]
pub use utils::{text_clean, text_collapse};
pub use utils::{text_remove_newlines, text_trim};

/// Max UTF-8 byte length for name metadata trimming.
#[cfg(feature = "meta-code")]
pub const META_TRIM_NAME: usize = 128;

/// Max UTF-8 byte length for description metadata trimming.
#[cfg(feature = "meta-code")]
pub const META_TRIM_DESCRIPTION: usize = 4096;

/// Max decoded payload size in bytes for the meta element.
#[cfg(feature = "meta-code")]
pub const META_TRIM_META: usize = 128_000;

/// Buffer size in bytes for streaming file reads (4 MB).
pub const IO_READ_SIZE: usize = 4_194_304;

/// Character n-gram width for text content features.
pub const TEXT_NGRAM_SIZE: usize = 13;

/// Error type for ISCC operations.
#[derive(Debug, thiserror::Error)]
pub enum IsccError {
    /// Input data is invalid.
    #[error("invalid input: {0}")]
    InvalidInput(String),
}

/// Result type alias for ISCC operations.
pub type IsccResult<T> = Result<T, IsccError>;

/// Interleave two 32-byte SimHash digests in 4-byte chunks.
///
/// Takes the first 16 bytes of each digest and interleaves them into
/// a 32-byte result: 4 bytes from `a`, 4 bytes from `b`, alternating
/// for 4 rounds (8 chunks total).
#[cfg(feature = "meta-code")]
fn interleave_digests(a: &[u8], b: &[u8]) -> Vec<u8> {
    let mut result = vec![0u8; 32];
    for chunk in 0..4 {
        let src = chunk * 4;
        let dst_a = chunk * 8;
        let dst_b = chunk * 8 + 4;
        result[dst_a..dst_a + 4].copy_from_slice(&a[src..src + 4]);
        result[dst_b..dst_b + 4].copy_from_slice(&b[src..src + 4]);
    }
    result
}

/// Compute a SimHash digest from the name text for meta hashing.
///
/// Applies `text_collapse`, generates width-3 sliding window n-grams,
/// hashes each with BLAKE3, and produces a SimHash.
#[cfg(feature = "meta-code")]
fn meta_name_simhash(name: &str) -> Vec<u8> {
    let collapsed_name = utils::text_collapse(name);
    let name_ngrams = simhash::sliding_window_strs(&collapsed_name, 3);
    let name_hashes: Vec<[u8; 32]> = name_ngrams
        .iter()
        .map(|ng| *blake3::hash(ng.as_bytes()).as_bytes())
        .collect();
    simhash::alg_simhash_inner(&name_hashes)
}

/// Compute a similarity-preserving 256-bit hash from metadata text.
///
/// Produces a SimHash digest from `name` n-grams. When `extra` is provided,
/// interleaves the name and extra SimHash digests in 4-byte chunks.
#[cfg(feature = "meta-code")]
fn soft_hash_meta_v0(name: &str, extra: Option<&str>) -> Vec<u8> {
    let name_simhash = meta_name_simhash(name);

    match extra {
        None | Some("") => name_simhash,
        Some(extra_str) => {
            let collapsed_extra = utils::text_collapse(extra_str);
            let extra_ngrams = simhash::sliding_window_strs(&collapsed_extra, 3);
            let extra_hashes: Vec<[u8; 32]> = extra_ngrams
                .iter()
                .map(|ng| *blake3::hash(ng.as_bytes()).as_bytes())
                .collect();
            let extra_simhash = simhash::alg_simhash_inner(&extra_hashes);

            interleave_digests(&name_simhash, &extra_simhash)
        }
    }
}

/// Compute a similarity-preserving 256-bit hash from name text and raw bytes.
///
/// Like `soft_hash_meta_v0` but the extra data is raw bytes instead of text.
/// Uses width-4 byte n-grams (no `text_collapse`) for the bytes path,
/// and interleaves name/bytes SimHash digests in 4-byte chunks.
#[cfg(feature = "meta-code")]
fn soft_hash_meta_v0_with_bytes(name: &str, extra: &[u8]) -> Vec<u8> {
    let name_simhash = meta_name_simhash(name);

    if extra.is_empty() {
        return name_simhash;
    }

    let byte_ngrams = simhash::sliding_window_bytes(extra, 4);
    let byte_hashes: Vec<[u8; 32]> = byte_ngrams
        .iter()
        .map(|ng| *blake3::hash(ng).as_bytes())
        .collect();
    let byte_simhash = simhash::alg_simhash_inner(&byte_hashes);

    interleave_digests(&name_simhash, &byte_simhash)
}

/// Decode a Data-URL's base64 payload.
///
/// Expects a string starting with `"data:"`. Splits on the first `,` and
/// decodes the remainder as standard base64. Returns `InvalidInput` on
/// missing comma or invalid base64.
#[cfg(feature = "meta-code")]
fn decode_data_url(data_url: &str) -> IsccResult<Vec<u8>> {
    let payload_b64 = data_url
        .split_once(',')
        .map(|(_, b64)| b64)
        .ok_or_else(|| IsccError::InvalidInput("Data-URL missing comma separator".into()))?;
    data_encoding::BASE64
        .decode(payload_b64.as_bytes())
        .map_err(|e| IsccError::InvalidInput(format!("invalid base64 in Data-URL: {e}")))
}

/// Parse a meta string as JSON and re-serialize to RFC 8785 (JCS) canonical bytes.
#[cfg(feature = "meta-code")]
fn parse_meta_json(meta_str: &str) -> IsccResult<Vec<u8>> {
    let parsed: serde_json::Value = serde_json::from_str(meta_str)
        .map_err(|e| IsccError::InvalidInput(format!("invalid JSON in meta: {e}")))?;
    let mut buf = Vec::new();
    serde_json_canonicalizer::to_writer(&parsed, &mut buf)
        .map_err(|e| IsccError::InvalidInput(format!("JSON canonicalization failed: {e}")))?;
    Ok(buf)
}

/// Build a Data-URL from canonical JSON bytes.
///
/// Uses `application/ld+json` media type if the JSON has an `@context` key,
/// otherwise `application/json`. Encodes payload as standard base64 with padding.
#[cfg(feature = "meta-code")]
fn build_meta_data_url(json_bytes: &[u8], json_value: &serde_json::Value) -> String {
    let media_type = if json_value.get("@context").is_some() {
        "application/ld+json"
    } else {
        "application/json"
    };
    let b64 = data_encoding::BASE64.encode(json_bytes);
    format!("data:{media_type};base64,{b64}")
}

/// Encode a raw digest into an ISCC unit string.
///
/// Takes integer type identifiers (matching `MainType`, `SubType`, `Version` enum values)
/// and a raw digest, returns a base32-encoded ISCC unit string.
///
/// # Errors
///
/// Returns `IsccError::InvalidInput` if enum values are out of range, if `mtype` is
/// `MainType::Iscc` (5), or if `digest.len() < bit_length / 8`.
pub fn encode_component(
    mtype: u8,
    stype: u8,
    version: u8,
    bit_length: u32,
    digest: &[u8],
) -> IsccResult<String> {
    let mt = codec::MainType::try_from(mtype)?;
    let st = codec::SubType::try_from(stype)?;
    let vs = codec::Version::try_from(version)?;
    let needed = (bit_length / 8) as usize;
    if digest.len() < needed {
        return Err(IsccError::InvalidInput(format!(
            "digest length {} < bit_length/8 ({})",
            digest.len(),
            needed
        )));
    }
    codec::encode_component(mt, st, vs, bit_length, digest)
}

/// Decode an ISCC unit string into its header components and raw digest.
///
/// Inverse of [`encode_component`]. Strips an optional `"ISCC:"` prefix and
/// dashes, base32-decodes the string, parses the variable-length header, and
/// returns the digest truncated to exactly the encoded bit-length.
///
/// Returns `(maintype, subtype, version, length_index, digest)` where the
/// integer fields match [`codec::MainType`], [`codec::SubType`], and
/// [`codec::Version`] enum values.
///
/// # Errors
///
/// Returns `IsccError::InvalidInput` on invalid base32 input, malformed
/// header, or if the decoded body is shorter than the expected digest length.
pub fn iscc_decode(iscc: &str) -> IsccResult<(u8, u8, u8, u8, Vec<u8>)> {
    // Strip optional "ISCC:" prefix (case-sensitive, matching iscc_decompose)
    let clean = iscc.strip_prefix("ISCC:").unwrap_or(iscc);
    // Remove dashes (matching iscc_clean behavior for base32 input)
    let clean = clean.replace('-', "");
    let raw = codec::decode_base32(&clean)?;
    let (mt, st, vs, length_index, tail) = codec::decode_header(&raw)?;
    let bit_length = codec::decode_length(mt, length_index, st);
    let nbytes = (bit_length / 8) as usize;
    if tail.len() < nbytes {
        return Err(IsccError::InvalidInput(format!(
            "decoded body too short: expected {nbytes} digest bytes, got {}",
            tail.len()
        )));
    }
    Ok((
        mt as u8,
        st as u8,
        vs as u8,
        length_index as u8,
        tail[..nbytes].to_vec(),
    ))
}

/// Convert a JSON string into a `data:` URL with JCS canonicalization.
///
/// Parses the JSON, re-serializes to [RFC 8785 (JCS)](https://www.rfc-editor.org/rfc/rfc8785)
/// canonical form, base64-encodes the result, and wraps it in a `data:` URL.
/// Uses `application/ld+json` media type when the JSON contains an `@context`
/// key, otherwise `application/json`.
///
/// This enables all language bindings to support dict/object meta parameters
/// by serializing to JSON once (language-specific) then delegating encoding
/// to Rust.
///
/// # Errors
///
/// Returns [`IsccError::InvalidInput`] if `json` is not valid JSON or if
/// JCS canonicalization fails.
///
/// # Examples
///
/// ```
/// # use iscc_lib::json_to_data_url;
/// let url = json_to_data_url(r#"{"key": "value"}"#).unwrap();
/// assert!(url.starts_with("data:application/json;base64,"));
///
/// let ld_url = json_to_data_url(r#"{"@context": "https://schema.org"}"#).unwrap();
/// assert!(ld_url.starts_with("data:application/ld+json;base64,"));
/// ```
#[cfg(feature = "meta-code")]
pub fn json_to_data_url(json: &str) -> IsccResult<String> {
    let parsed: serde_json::Value = serde_json::from_str(json)
        .map_err(|e| IsccError::InvalidInput(format!("invalid JSON: {e}")))?;
    let mut canonical_bytes = Vec::new();
    serde_json_canonicalizer::to_writer(&parsed, &mut canonical_bytes)
        .map_err(|e| IsccError::InvalidInput(format!("JSON canonicalization failed: {e}")))?;
    Ok(build_meta_data_url(&canonical_bytes, &parsed))
}

/// Generate a Meta-Code from name and optional metadata.
///
/// Produces an ISCC Meta-Code by hashing the provided name, description,
/// and metadata fields using the SimHash algorithm. When `meta` is provided,
/// it is treated as either a Data-URL (if starting with `"data:"`) or a JSON
/// string, and the decoded/serialized bytes are used for similarity hashing
/// and metahash computation.
#[cfg(feature = "meta-code")]
pub fn gen_meta_code_v0(
    name: &str,
    description: Option<&str>,
    meta: Option<&str>,
    bits: u32,
) -> IsccResult<MetaCodeResult> {
    // Normalize name: clean → remove newlines → trim to 128 bytes
    let name = utils::text_clean(name);
    let name = utils::text_remove_newlines(&name);
    let name = utils::text_trim(&name, META_TRIM_NAME);

    if name.is_empty() {
        return Err(IsccError::InvalidInput(
            "name is empty after normalization".into(),
        ));
    }

    // Normalize description: clean → trim to 4096 bytes
    let desc_str = description.unwrap_or("");
    let desc_clean = utils::text_clean(desc_str);
    let desc_clean = utils::text_trim(&desc_clean, META_TRIM_DESCRIPTION);

    // Pre-decode fast check: reject obviously oversized meta strings
    if let Some(meta_str) = meta {
        const PRE_DECODE_LIMIT: usize = META_TRIM_META * 4 / 3 + 256;
        if meta_str.len() > PRE_DECODE_LIMIT {
            return Err(IsccError::InvalidInput(format!(
                "meta string exceeds size limit ({} > {PRE_DECODE_LIMIT} bytes)",
                meta_str.len()
            )));
        }
    }

    // Resolve meta payload bytes (if meta is provided)
    let meta_payload: Option<Vec<u8>> = match meta {
        Some(meta_str) if meta_str.starts_with("data:") => Some(decode_data_url(meta_str)?),
        Some(meta_str) => Some(parse_meta_json(meta_str)?),
        None => None,
    };

    // Post-decode check: reject payloads exceeding META_TRIM_META
    if let Some(ref payload) = meta_payload {
        if payload.len() > META_TRIM_META {
            return Err(IsccError::InvalidInput(format!(
                "decoded meta payload exceeds size limit ({} > {META_TRIM_META} bytes)",
                payload.len()
            )));
        }
    }

    // Branch: meta bytes path vs. description text path
    if let Some(ref payload) = meta_payload {
        let meta_code_digest = soft_hash_meta_v0_with_bytes(&name, payload);
        let metahash = utils::multi_hash_blake3(payload);

        let meta_code = codec::encode_component(
            codec::MainType::Meta,
            codec::SubType::None,
            codec::Version::V0,
            bits,
            &meta_code_digest,
        )?;

        // Build the meta Data-URL for the result
        let meta_value = match meta {
            Some(meta_str) if meta_str.starts_with("data:") => meta_str.to_string(),
            Some(meta_str) => {
                let parsed: serde_json::Value = serde_json::from_str(meta_str)
                    .map_err(|e| IsccError::InvalidInput(format!("invalid JSON: {e}")))?;
                build_meta_data_url(payload, &parsed)
            }
            None => unreachable!(),
        };

        Ok(MetaCodeResult {
            iscc: format!("ISCC:{meta_code}"),
            name: name.clone(),
            description: if desc_clean.is_empty() {
                None
            } else {
                Some(desc_clean)
            },
            meta: Some(meta_value),
            metahash,
        })
    } else {
        // Compute metahash from normalized text payload
        let payload = if desc_clean.is_empty() {
            name.clone()
        } else {
            format!("{name} {desc_clean}")
        };
        let payload = payload.trim().to_string();
        let metahash = utils::multi_hash_blake3(payload.as_bytes());

        // Compute similarity digest
        let extra = if desc_clean.is_empty() {
            None
        } else {
            Some(desc_clean.as_str())
        };
        let meta_code_digest = soft_hash_meta_v0(&name, extra);

        let meta_code = codec::encode_component(
            codec::MainType::Meta,
            codec::SubType::None,
            codec::Version::V0,
            bits,
            &meta_code_digest,
        )?;

        Ok(MetaCodeResult {
            iscc: format!("ISCC:{meta_code}"),
            name: name.clone(),
            description: if desc_clean.is_empty() {
                None
            } else {
                Some(desc_clean)
            },
            meta: None,
            metahash,
        })
    }
}

/// Compute a 256-bit similarity-preserving hash from collapsed text.
///
/// Generates character n-grams with a sliding window of width 13,
/// hashes each with xxh32, then applies MinHash to produce a 32-byte digest.
#[cfg(feature = "text-processing")]
fn soft_hash_text_v0(text: &str) -> Vec<u8> {
    let ngrams = simhash::sliding_window_strs(text, TEXT_NGRAM_SIZE);
    let features: Vec<u32> = ngrams
        .iter()
        .map(|ng| xxhash_rust::xxh32::xxh32(ng.as_bytes(), 0))
        .collect();
    minhash::alg_minhash_256(&features)
}

/// Generate a Text-Code from plain text content.
///
/// Produces an ISCC Content-Code for text by collapsing the input,
/// extracting character n-gram features, and applying MinHash to
/// create a similarity-preserving fingerprint.
#[cfg(feature = "text-processing")]
pub fn gen_text_code_v0(text: &str, bits: u32) -> IsccResult<TextCodeResult> {
    let collapsed = utils::text_collapse(text);
    let characters = collapsed.chars().count();
    let hash_digest = soft_hash_text_v0(&collapsed);
    let component = codec::encode_component(
        codec::MainType::Content,
        codec::SubType::TEXT,
        codec::Version::V0,
        bits,
        &hash_digest,
    )?;
    Ok(TextCodeResult {
        iscc: format!("ISCC:{component}"),
        characters,
    })
}

/// Transpose a matrix represented as a Vec of Vecs.
fn transpose_matrix(matrix: &[Vec<f64>]) -> Vec<Vec<f64>> {
    let rows = matrix.len();
    if rows == 0 {
        return vec![];
    }
    let cols = matrix[0].len();
    let mut result = vec![vec![0.0f64; rows]; cols];
    for (r, row) in matrix.iter().enumerate() {
        for (c, &val) in row.iter().enumerate() {
            result[c][r] = val;
        }
    }
    result
}

/// Extract an 8×8 block from a matrix and flatten to 64 values.
///
/// Block position `(col, row)` means the block starts at
/// `matrix[row][col]` and spans 8 rows and 8 columns.
fn flatten_8x8(matrix: &[Vec<f64>], col: usize, row: usize) -> Vec<f64> {
    let mut flat = Vec::with_capacity(64);
    for matrix_row in matrix.iter().skip(row).take(8) {
        for &val in matrix_row.iter().skip(col).take(8) {
            flat.push(val);
        }
    }
    flat
}

/// Compute the median of a slice of f64 values.
///
/// For even-length slices, returns the average of the two middle values
/// (matching Python `statistics.median` behavior).
fn compute_median(values: &[f64]) -> f64 {
    let mut sorted: Vec<f64> = values.to_vec();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
    let n = sorted.len();
    if n % 2 == 1 {
        sorted[n / 2]
    } else {
        (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
    }
}

/// Convert a slice of bools to a byte vector (MSB first per byte).
fn bits_to_bytes(bits: &[bool]) -> Vec<u8> {
    bits.chunks(8)
        .map(|chunk| {
            let mut byte = 0u8;
            for (i, &bit) in chunk.iter().enumerate() {
                if bit {
                    byte |= 1 << (7 - i);
                }
            }
            byte
        })
        .collect()
}

/// Compute a DCT-based perceptual hash from 32×32 grayscale pixels.
///
/// Applies a 2D DCT to the pixel matrix, extracts four 8×8 low-frequency
/// blocks, and generates a bitstring by comparing each coefficient against
/// the block median. Returns up to `bits` bits as a byte vector.
fn soft_hash_image_v0(pixels: &[u8], bits: u32) -> IsccResult<Vec<u8>> {
    if pixels.len() != 1024 {
        return Err(IsccError::InvalidInput(format!(
            "expected 1024 pixels, got {}",
            pixels.len()
        )));
    }
    if bits > 256 {
        return Err(IsccError::InvalidInput(format!(
            "bits must be <= 256, got {bits}"
        )));
    }

    // Step 1: Row-wise DCT (32 rows of 32 pixels)
    let rows: Vec<Vec<f64>> = pixels
        .chunks(32)
        .map(|row| {
            let row_f64: Vec<f64> = row.iter().map(|&p| p as f64).collect();
            dct::alg_dct(&row_f64)
        })
        .collect::<IsccResult<Vec<Vec<f64>>>>()?;

    // Step 2: Transpose
    let transposed = transpose_matrix(&rows);

    // Step 3: Column-wise DCT
    let dct_cols: Vec<Vec<f64>> = transposed
        .iter()
        .map(|col| dct::alg_dct(col))
        .collect::<IsccResult<Vec<Vec<f64>>>>()?;

    // Step 4: Transpose back → dct_matrix
    let dct_matrix = transpose_matrix(&dct_cols);

    // Step 5: Extract 8×8 blocks at positions (0,0), (1,0), (0,1), (1,1)
    let positions = [(0, 0), (1, 0), (0, 1), (1, 1)];
    let mut bitstring = Vec::<bool>::with_capacity(256);

    for (col, row) in positions {
        let flat = flatten_8x8(&dct_matrix, col, row);
        let median = compute_median(&flat);
        for val in &flat {
            bitstring.push(*val > median);
        }
        if bitstring.len() >= bits as usize {
            break;
        }
    }

    // Step 6: Convert first `bits` bools to bytes
    Ok(bits_to_bytes(&bitstring[..bits as usize]))
}

/// Generate an Image-Code from pixel data.
///
/// Produces an ISCC Content-Code for images from a sequence of 1024
/// grayscale pixel values (32×32, values 0-255) using a DCT-based
/// perceptual hash.
pub fn gen_image_code_v0(pixels: &[u8], bits: u32) -> IsccResult<ImageCodeResult> {
    let hash_digest = soft_hash_image_v0(pixels, bits)?;
    let component = codec::encode_component(
        codec::MainType::Content,
        codec::SubType::Image,
        codec::Version::V0,
        bits,
        &hash_digest,
    )?;
    Ok(ImageCodeResult {
        iscc: format!("ISCC:{component}"),
    })
}

/// Split a slice into `n` parts, distributing remainder across first chunks.
///
/// Equivalent to `numpy.array_split` / `more_itertools.divide`:
/// each part gets `len / n` elements, and the first `len % n` parts
/// get one extra element. Returns empty slices for excess parts.
fn array_split<T>(slice: &[T], n: usize) -> Vec<&[T]> {
    if n == 0 {
        return vec![];
    }
    let len = slice.len();
    let base = len / n;
    let remainder = len % n;
    let mut parts = Vec::with_capacity(n);
    let mut offset = 0;
    for i in 0..n {
        let size = base + if i < remainder { 1 } else { 0 };
        parts.push(&slice[offset..offset + size]);
        offset += size;
    }
    parts
}

/// Compute a multi-stage SimHash digest from Chromaprint features.
///
/// Builds a 32-byte digest by concatenating 4-byte SimHash chunks:
/// - Stage 1: overall SimHash of all features (4 bytes)
/// - Stage 2: SimHash of each quarter of features (4 × 4 = 16 bytes)
/// - Stage 3: SimHash of each third of sorted features (3 × 4 = 12 bytes)
fn soft_hash_audio_v0(cv: &[i32]) -> Vec<u8> {
    // Convert each i32 to 4-byte big-endian digest
    let digests: Vec<[u8; 4]> = cv.iter().map(|&v| v.to_be_bytes()).collect();

    if digests.is_empty() {
        return vec![0u8; 32];
    }

    // Stage 1: overall SimHash (4 bytes)
    let mut parts: Vec<u8> = simhash::alg_simhash_inner(&digests);

    // Stage 2: quarter-based SimHashes (4 × 4 = 16 bytes)
    let quarters = array_split(&digests, 4);
    for quarter in &quarters {
        if quarter.is_empty() {
            parts.extend_from_slice(&[0u8; 4]);
        } else {
            parts.extend_from_slice(&simhash::alg_simhash_inner(quarter));
        }
    }

    // Stage 3: sorted-third-based SimHashes (3 × 4 = 12 bytes)
    let mut sorted_values: Vec<i32> = cv.to_vec();
    sorted_values.sort();
    let sorted_digests: Vec<[u8; 4]> = sorted_values.iter().map(|&v| v.to_be_bytes()).collect();
    let thirds = array_split(&sorted_digests, 3);
    for third in &thirds {
        if third.is_empty() {
            parts.extend_from_slice(&[0u8; 4]);
        } else {
            parts.extend_from_slice(&simhash::alg_simhash_inner(third));
        }
    }

    parts
}

/// Generate an Audio-Code from a Chromaprint feature vector.
///
/// Produces an ISCC Content-Code for audio from a Chromaprint signed
/// integer fingerprint vector using multi-stage SimHash.
pub fn gen_audio_code_v0(cv: &[i32], bits: u32) -> IsccResult<AudioCodeResult> {
    let hash_digest = soft_hash_audio_v0(cv);
    let component = codec::encode_component(
        codec::MainType::Content,
        codec::SubType::Audio,
        codec::Version::V0,
        bits,
        &hash_digest,
    )?;
    Ok(AudioCodeResult {
        iscc: format!("ISCC:{component}"),
    })
}

/// Compute a similarity-preserving hash from video frame signatures.
///
/// Deduplicates frame signatures, computes column-wise sums across all
/// unique frames, then applies WTA-Hash to produce a digest of `bits/8` bytes.
pub fn soft_hash_video_v0<S: AsRef<[i32]> + Ord>(
    frame_sigs: &[S],
    bits: u32,
) -> IsccResult<Vec<u8>> {
    if frame_sigs.is_empty() {
        return Err(IsccError::InvalidInput(
            "frame_sigs must not be empty".into(),
        ));
    }

    // Deduplicate using BTreeSet (S: Ord)
    let unique: std::collections::BTreeSet<&S> = frame_sigs.iter().collect();

    // Column-wise sum into i64 to avoid overflow
    let cols = frame_sigs[0].as_ref().len();
    let mut vecsum = vec![0i64; cols];
    for sig in &unique {
        for (c, &val) in sig.as_ref().iter().enumerate() {
            vecsum[c] += val as i64;
        }
    }

    wtahash::alg_wtahash(&vecsum, bits)
}

/// Generate a Video-Code from frame signature data.
///
/// Produces an ISCC Content-Code for video from a sequence of MPEG-7 frame
/// signatures. Each frame signature is a 380-element integer vector.
pub fn gen_video_code_v0<S: AsRef<[i32]> + Ord>(
    frame_sigs: &[S],
    bits: u32,
) -> IsccResult<VideoCodeResult> {
    let digest = soft_hash_video_v0(frame_sigs, bits)?;
    let component = codec::encode_component(
        codec::MainType::Content,
        codec::SubType::Video,
        codec::Version::V0,
        bits,
        &digest,
    )?;
    Ok(VideoCodeResult {
        iscc: format!("ISCC:{component}"),
    })
}

/// Combine multiple Content-Code digests into a single similarity hash.
///
/// Takes raw decoded ISCC bytes (header + body) for each Content-Code and
/// produces a SimHash digest. Each input is trimmed to `bits/8` bytes by
/// keeping the first header byte (encodes type info) plus `nbytes-1` body bytes.
/// Requires at least 2 codes, all of MainType::Content.
fn soft_hash_codes_v0(cc_digests: &[Vec<u8>], bits: u32) -> IsccResult<Vec<u8>> {
    if cc_digests.len() < 2 {
        return Err(IsccError::InvalidInput(
            "at least 2 Content-Codes required for mixing".into(),
        ));
    }

    let nbytes = (bits / 8) as usize;
    let mut prepared: Vec<Vec<u8>> = Vec::with_capacity(cc_digests.len());

    for raw in cc_digests {
        let (mtype, stype, _ver, blen, body) = codec::decode_header(raw)?;
        if mtype != codec::MainType::Content {
            return Err(IsccError::InvalidInput(
                "all codes must be Content-Codes".into(),
            ));
        }
        let unit_bits = codec::decode_length(mtype, blen, stype);
        if unit_bits < bits {
            return Err(IsccError::InvalidInput(format!(
                "Content-Code too short for {bits}-bit length (has {unit_bits} bits)"
            )));
        }
        let mut entry = Vec::with_capacity(nbytes);
        entry.push(raw[0]); // first byte preserves type info
        let take = std::cmp::min(nbytes - 1, body.len());
        entry.extend_from_slice(&body[..take]);
        // Pad with zeros if body is shorter than nbytes-1
        while entry.len() < nbytes {
            entry.push(0);
        }
        prepared.push(entry);
    }

    Ok(simhash::alg_simhash_inner(&prepared))
}

/// Generate a Mixed-Code from multiple Content-Code strings.
///
/// Produces a Mixed Content-Code by combining multiple ISCC Content-Codes
/// of different types (text, image, audio, video) using SimHash. Input codes
/// may optionally include the "ISCC:" prefix.
pub fn gen_mixed_code_v0(codes: &[&str], bits: u32) -> IsccResult<MixedCodeResult> {
    let decoded: Vec<Vec<u8>> = codes
        .iter()
        .map(|code| {
            let clean = code.strip_prefix("ISCC:").unwrap_or(code);
            codec::decode_base32(clean)
        })
        .collect::<IsccResult<Vec<Vec<u8>>>>()?;

    let digest = soft_hash_codes_v0(&decoded, bits)?;

    let component = codec::encode_component(
        codec::MainType::Content,
        codec::SubType::Mixed,
        codec::Version::V0,
        bits,
        &digest,
    )?;

    Ok(MixedCodeResult {
        iscc: format!("ISCC:{component}"),
        parts: codes.iter().map(|s| s.to_string()).collect(),
    })
}

/// Generate a Data-Code from raw byte data.
///
/// Produces an ISCC Data-Code by splitting data into content-defined chunks,
/// hashing each chunk with xxh32, and applying MinHash to create a
/// similarity-preserving fingerprint.
pub fn gen_data_code_v0(data: &[u8], bits: u32) -> IsccResult<DataCodeResult> {
    let chunks = cdc::alg_cdc_chunks_unchecked(data, false, cdc::DATA_AVG_CHUNK_SIZE);
    let mut features: Vec<u32> = chunks
        .iter()
        .map(|chunk| xxhash_rust::xxh32::xxh32(chunk, 0))
        .collect();

    // Defensive: ensure at least one feature (alg_cdc_chunks guarantees >= 1 chunk)
    if features.is_empty() {
        features.push(xxhash_rust::xxh32::xxh32(b"", 0));
    }

    let digest = minhash::alg_minhash_256(&features);
    let component = codec::encode_component(
        codec::MainType::Data,
        codec::SubType::None,
        codec::Version::V0,
        bits,
        &digest,
    )?;

    Ok(DataCodeResult {
        iscc: format!("ISCC:{component}"),
    })
}

/// Generate an Instance-Code from raw byte data.
///
/// Produces an ISCC Instance-Code by hashing the complete byte stream
/// with BLAKE3. Captures the exact binary identity of the data.
pub fn gen_instance_code_v0(data: &[u8], bits: u32) -> IsccResult<InstanceCodeResult> {
    let digest = blake3::hash(data);
    let datahash = utils::multi_hash_blake3(data);
    let filesize = data.len() as u64;
    let component = codec::encode_component(
        codec::MainType::Instance,
        codec::SubType::None,
        codec::Version::V0,
        bits,
        digest.as_bytes(),
    )?;
    Ok(InstanceCodeResult {
        iscc: format!("ISCC:{component}"),
        datahash,
        filesize,
    })
}

/// Generate a composite ISCC-CODE from individual ISCC unit codes.
///
/// Combines multiple ISCC unit codes (Meta-Code, Content-Code, Data-Code,
/// Instance-Code) into a single composite ISCC-CODE. Input codes may
/// optionally include the "ISCC:" prefix. At least Data-Code and
/// Instance-Code are required. When `wide` is true and exactly two
/// 128-bit+ codes (Data + Instance) are provided, produces a 256-bit
/// wide-mode code.
pub fn gen_iscc_code_v0(codes: &[&str], wide: bool) -> IsccResult<IsccCodeResult> {
    // Step 1: Clean inputs — strip "ISCC:" prefix
    let cleaned: Vec<&str> = codes
        .iter()
        .map(|c| c.strip_prefix("ISCC:").unwrap_or(c))
        .collect();

    // Step 2: Validate minimum count
    if cleaned.len() < 2 {
        return Err(IsccError::InvalidInput(
            "at least 2 ISCC unit codes required".into(),
        ));
    }

    // Step 3: Validate minimum length (16 base32 chars = 64-bit minimum)
    for code in &cleaned {
        if code.len() < 16 {
            return Err(IsccError::InvalidInput(format!(
                "ISCC unit code too short (min 16 chars): {code}"
            )));
        }
    }

    // Step 4: Decode each code
    let mut decoded: Vec<(
        codec::MainType,
        codec::SubType,
        codec::Version,
        u32,
        Vec<u8>,
    )> = Vec::with_capacity(cleaned.len());
    for code in &cleaned {
        let raw = codec::decode_base32(code)?;
        let header = codec::decode_header(&raw)?;
        decoded.push(header);
    }

    // Step 5: Sort by MainType (ascending)
    decoded.sort_by_key(|&(mt, ..)| mt);

    // Step 6: Extract main_types
    let main_types: Vec<codec::MainType> = decoded.iter().map(|&(mt, ..)| mt).collect();

    // Step 7: Validate last two are Data + Instance (mandatory)
    let n = main_types.len();
    if main_types[n - 2] != codec::MainType::Data || main_types[n - 1] != codec::MainType::Instance
    {
        return Err(IsccError::InvalidInput(
            "Data-Code and Instance-Code are mandatory".into(),
        ));
    }

    // Step 8: Determine wide composite
    let is_wide = wide
        && decoded.len() == 2
        && main_types == [codec::MainType::Data, codec::MainType::Instance]
        && decoded
            .iter()
            .all(|&(mt, st, _, len, _)| codec::decode_length(mt, len, st) >= 128);

    // Step 9: Determine SubType
    let st = if is_wide {
        codec::SubType::Wide
    } else {
        // Collect SubTypes of Semantic/Content units
        let sc_subtypes: Vec<codec::SubType> = decoded
            .iter()
            .filter(|&&(mt, ..)| mt == codec::MainType::Semantic || mt == codec::MainType::Content)
            .map(|&(_, st, ..)| st)
            .collect();

        if !sc_subtypes.is_empty() {
            // All must be the same
            let first = sc_subtypes[0];
            if sc_subtypes.iter().all(|&s| s == first) {
                first
            } else {
                return Err(IsccError::InvalidInput(
                    "mixed SubTypes among Content/Semantic units".into(),
                ));
            }
        } else if decoded.len() == 2 {
            codec::SubType::Sum
        } else {
            codec::SubType::IsccNone
        }
    };

    // Step 10–11: Get optional MainTypes and encode
    let optional_types = &main_types[..n - 2];
    let encoded_length = codec::encode_units(optional_types)?;

    // Step 12: Build digest body
    let bytes_per_unit = if is_wide { 16 } else { 8 };
    let mut digest = Vec::with_capacity(decoded.len() * bytes_per_unit);
    for (_, _, _, _, tail) in &decoded {
        let take = bytes_per_unit.min(tail.len());
        digest.extend_from_slice(&tail[..take]);
    }

    // Step 13–14: Encode header + digest as base32
    let header = codec::encode_header(
        codec::MainType::Iscc,
        st,
        codec::Version::V0,
        encoded_length,
    )?;
    let mut code_bytes = header;
    code_bytes.extend_from_slice(&digest);
    let code = codec::encode_base32(&code_bytes);

    // Step 15: Return with prefix
    Ok(IsccCodeResult {
        iscc: format!("ISCC:{code}"),
    })
}

/// Generate a composite ISCC-CODE from a file in a single pass.
///
/// Opens the file at `path`, reads it with an optimal buffer size, and feeds
/// both `DataHasher` (CDC/MinHash) and `InstanceHasher` (BLAKE3) from the
/// same read buffer. Composes the final ISCC-CODE from the Data-Code and
/// Instance-Code internally. This avoids multiple passes over the file and
/// eliminates per-chunk FFI overhead in language bindings.
///
/// When `add_units` is `true`, the result includes the individual Data-Code
/// and Instance-Code ISCC strings at the requested `bits` precision.
pub fn gen_sum_code_v0(
    path: &std::path::Path,
    bits: u32,
    wide: bool,
    add_units: bool,
) -> IsccResult<SumCodeResult> {
    use std::io::Read;

    let mut file = std::fs::File::open(path)
        .map_err(|e| IsccError::InvalidInput(format!("Cannot open file: {e}")))?;

    let mut hasher = streaming::SumHasher::new();

    let mut buf = vec![0u8; IO_READ_SIZE];
    loop {
        let n = file
            .read(&mut buf)
            .map_err(|e| IsccError::InvalidInput(format!("Cannot read file: {e}")))?;
        if n == 0 {
            break;
        }
        hasher.update(&buf[..n]);
    }

    hasher.finalize(bits, wide, add_units)
}

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

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_title_only() {
        let result = gen_meta_code_v0("Die Unendliche Geschichte", None, None, 64).unwrap();
        assert_eq!(result.iscc, "ISCC:AAAZXZ6OU74YAZIM");
        assert_eq!(result.name, "Die Unendliche Geschichte");
        assert_eq!(result.description, None);
        assert_eq!(result.meta, None);
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_title_description() {
        let result = gen_meta_code_v0(
            "Die Unendliche Geschichte",
            Some("Von Michael Ende"),
            None,
            64,
        )
        .unwrap();
        assert_eq!(result.iscc, "ISCC:AAAZXZ6OU4E45RB5");
        assert_eq!(result.name, "Die Unendliche Geschichte");
        assert_eq!(result.description, Some("Von Michael Ende".to_string()));
        assert_eq!(result.meta, None);
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_json_meta() {
        let result = gen_meta_code_v0("Hello", None, Some(r#"{"some":"object"}"#), 64).unwrap();
        assert_eq!(result.iscc, "ISCC:AAAWKLHFXN63LHL2");
        assert!(result.meta.is_some());
        assert!(
            result
                .meta
                .unwrap()
                .starts_with("data:application/json;base64,")
        );
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_data_url_meta() {
        let result = gen_meta_code_v0(
            "Hello",
            None,
            Some("data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9"),
            64,
        )
        .unwrap();
        assert_eq!(result.iscc, "ISCC:AAAWKLHFXN43ICP2");
        // Data-URL is passed through as-is
        assert_eq!(
            result.meta,
            Some("data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9".to_string())
        );
    }

    /// Verify that JSON metadata with float values is canonicalized per RFC 8785 (JCS).
    ///
    /// JCS serializes `1.0` as `1` (integer form), while `serde_json` preserves `1.0`.
    /// This causes different canonical bytes, different metahash, and different ISCC codes.
    /// Expected values generated by `iscc-core` with `jcs.canonicalize({"value": 1.0})`.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_jcs_float_canonicalization() {
        // JCS canonicalizes {"value": 1.0} → {"value":1} (integer form)
        // serde_json produces {"value":1.0} (preserves float notation)
        let result = gen_meta_code_v0("Test", None, Some(r#"{"value":1.0}"#), 64).unwrap();

        // Expected values from iscc-core (Python) using jcs.canonicalize()
        assert_eq!(
            result.iscc, "ISCC:AAAX4GX3RZH2I6QZ",
            "ISCC mismatch: parse_meta_json must use RFC 8785 (JCS) canonicalization"
        );
        assert_eq!(
            result.meta,
            Some("data:application/json;base64,eyJ2YWx1ZSI6MX0=".to_string()),
            "meta Data-URL mismatch: JCS should serialize 1.0 as 1"
        );
        assert_eq!(
            result.metahash, "1e2010b291d392b6999ffe4aa4661fb343fc371fca3bfb5bb4e8d8226fdf85743232",
            "metahash mismatch: canonical bytes differ between JCS and serde_json"
        );
    }

    /// Verify JCS number formatting for large floats (scientific notation edge case).
    ///
    /// JCS serializes `1e20` as `100000000000000000000` (expanded integer form).
    /// Expected values generated by `iscc-core` with `jcs.canonicalize({"value": 1e20})`.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_jcs_large_float_canonicalization() {
        let result = gen_meta_code_v0("Test", None, Some(r#"{"value":1e20}"#), 64).unwrap();

        assert_eq!(
            result.iscc, "ISCC:AAAX4GX3R32YH5P7",
            "ISCC mismatch: JCS should expand 1e20 to 100000000000000000000"
        );
        assert_eq!(
            result.meta,
            Some(
                "data:application/json;base64,eyJ2YWx1ZSI6MTAwMDAwMDAwMDAwMDAwMDAwMDAwfQ=="
                    .to_string()
            ),
            "meta Data-URL mismatch: JCS should expand large float to integer form"
        );
        assert_eq!(
            result.metahash, "1e201ff83c1822c348717658a0b4713739646da7c59832691b337a457416ddd1c73d",
            "metahash mismatch: canonical bytes differ for large float"
        );
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_invalid_json() {
        assert!(matches!(
            gen_meta_code_v0("test", None, Some("not json"), 64),
            Err(IsccError::InvalidInput(_))
        ));
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_invalid_data_url() {
        assert!(matches!(
            gen_meta_code_v0("test", None, Some("data:no-comma-here"), 64),
            Err(IsccError::InvalidInput(_))
        ));
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_meta_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let input_name = inputs[0].as_str().unwrap();
            let input_desc = inputs[1].as_str().unwrap();
            let meta_val = &inputs[2];
            let bits = inputs[3].as_u64().unwrap() as u32;

            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
            let expected_metahash = tc["outputs"]["metahash"].as_str().unwrap();

            // Dispatch meta parameter based on JSON value type
            let meta_arg: Option<String> = match meta_val {
                serde_json::Value::Null => None,
                serde_json::Value::String(s) => Some(s.clone()),
                serde_json::Value::Object(_) => Some(serde_json::to_string(meta_val).unwrap()),
                other => panic!("unexpected meta type in {tc_name}: {other:?}"),
            };

            let desc = if input_desc.is_empty() {
                None
            } else {
                Some(input_desc)
            };

            // Verify ISCC output from struct
            let result = gen_meta_code_v0(input_name, desc, meta_arg.as_deref(), bits)
                .unwrap_or_else(|e| panic!("gen_meta_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            // Verify metahash from struct
            assert_eq!(
                result.metahash, expected_metahash,
                "metahash mismatch in test case {tc_name}"
            );

            // Verify name from struct
            if let Some(expected_name) = tc["outputs"].get("name") {
                let expected_name = expected_name.as_str().unwrap();
                assert_eq!(
                    result.name, expected_name,
                    "name mismatch in test case {tc_name}"
                );
            }

            // Verify description from struct
            if let Some(expected_desc) = tc["outputs"].get("description") {
                let expected_desc = expected_desc.as_str().unwrap();
                assert_eq!(
                    result.description.as_deref(),
                    Some(expected_desc),
                    "description mismatch in test case {tc_name}"
                );
            }

            // Verify meta from struct
            if meta_arg.is_some() {
                assert!(
                    result.meta.is_some(),
                    "meta should be present in test case {tc_name}"
                );
            } else {
                assert!(
                    result.meta.is_none(),
                    "meta should be absent in test case {tc_name}"
                );
            }

            tested += 1;
        }

        assert_eq!(tested, 20, "expected 20 conformance tests to run");
    }

    #[cfg(feature = "text-processing")]
    #[test]
    fn test_gen_text_code_v0_empty() {
        let result = gen_text_code_v0("", 64).unwrap();
        assert_eq!(result.iscc, "ISCC:EAASL4F2WZY7KBXB");
        assert_eq!(result.characters, 0);
    }

    #[cfg(feature = "text-processing")]
    #[test]
    fn test_gen_text_code_v0_hello_world() {
        let result = gen_text_code_v0("Hello World", 64).unwrap();
        assert_eq!(result.iscc, "ISCC:EAASKDNZNYGUUF5A");
        assert_eq!(result.characters, 10); // "helloworld" after collapse
    }

    #[cfg(feature = "text-processing")]
    #[test]
    fn test_gen_text_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_text_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let input_text = inputs[0].as_str().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;

            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
            let expected_chars = tc["outputs"]["characters"].as_u64().unwrap() as usize;

            // Verify ISCC output from struct
            let result = gen_text_code_v0(input_text, bits)
                .unwrap_or_else(|e| panic!("gen_text_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            // Verify character count from struct
            assert_eq!(
                result.characters, expected_chars,
                "character count mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 5, "expected 5 conformance tests to run");
    }

    #[test]
    fn test_gen_image_code_v0_all_black() {
        let pixels = vec![0u8; 1024];
        let result = gen_image_code_v0(&pixels, 64).unwrap();
        assert_eq!(result.iscc, "ISCC:EEAQAAAAAAAAAAAA");
    }

    #[test]
    fn test_gen_image_code_v0_all_white() {
        let pixels = vec![255u8; 1024];
        let result = gen_image_code_v0(&pixels, 128).unwrap();
        assert_eq!(result.iscc, "ISCC:EEBYAAAAAAAAAAAAAAAAAAAAAAAAA");
    }

    #[test]
    fn test_gen_image_code_v0_invalid_pixel_count() {
        assert!(gen_image_code_v0(&[0u8; 100], 64).is_err());
    }

    #[test]
    fn test_gen_image_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_image_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let pixels_json = inputs[0].as_array().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();

            let pixels: Vec<u8> = pixels_json
                .iter()
                .map(|v| v.as_u64().unwrap() as u8)
                .collect();

            let result = gen_image_code_v0(&pixels, bits)
                .unwrap_or_else(|e| panic!("gen_image_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 3, "expected 3 conformance tests to run");
    }

    #[test]
    fn test_gen_audio_code_v0_empty() {
        let result = gen_audio_code_v0(&[], 64).unwrap();
        assert_eq!(result.iscc, "ISCC:EIAQAAAAAAAAAAAA");
    }

    #[test]
    fn test_gen_audio_code_v0_single() {
        let result = gen_audio_code_v0(&[1], 128).unwrap();
        assert_eq!(result.iscc, "ISCC:EIBQAAAAAEAAAAABAAAAAAAAAAAAA");
    }

    #[test]
    fn test_gen_audio_code_v0_negative() {
        let result = gen_audio_code_v0(&[-1, 0, 1], 256).unwrap();
        assert_eq!(
            result.iscc,
            "ISCC:EIDQAAAAAH777777AAAAAAAAAAAACAAAAAAP777774AAAAAAAAAAAAI"
        );
    }

    #[test]
    fn test_gen_audio_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_audio_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let cv_json = inputs[0].as_array().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();

            let cv: Vec<i32> = cv_json.iter().map(|v| v.as_i64().unwrap() as i32).collect();

            let result = gen_audio_code_v0(&cv, bits)
                .unwrap_or_else(|e| panic!("gen_audio_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 5, "expected 5 conformance tests to run");
    }

    #[test]
    fn test_array_split_even() {
        let data = vec![1, 2, 3, 4];
        let parts = array_split(&data, 4);
        assert_eq!(parts, vec![&[1][..], &[2][..], &[3][..], &[4][..]]);
    }

    #[test]
    fn test_array_split_remainder() {
        let data = vec![1, 2, 3, 4, 5];
        let parts = array_split(&data, 3);
        assert_eq!(parts, vec![&[1, 2][..], &[3, 4][..], &[5][..]]);
    }

    #[test]
    fn test_array_split_more_parts_than_elements() {
        let data = vec![1, 2];
        let parts = array_split(&data, 4);
        assert_eq!(
            parts,
            vec![&[1][..], &[2][..], &[][..] as &[i32], &[][..] as &[i32]]
        );
    }

    #[test]
    fn test_array_split_empty() {
        let data: Vec<i32> = vec![];
        let parts = array_split(&data, 3);
        assert_eq!(
            parts,
            vec![&[][..] as &[i32], &[][..] as &[i32], &[][..] as &[i32]]
        );
    }

    #[test]
    fn test_gen_video_code_v0_empty_frames() {
        let frames: Vec<Vec<i32>> = vec![];
        assert!(matches!(
            gen_video_code_v0(&frames, 64),
            Err(IsccError::InvalidInput(_))
        ));
    }

    #[test]
    fn test_gen_video_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_video_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let frames_json = inputs[0].as_array().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();

            let frame_sigs: Vec<Vec<i32>> = frames_json
                .iter()
                .map(|frame| {
                    frame
                        .as_array()
                        .unwrap()
                        .iter()
                        .map(|v| v.as_i64().unwrap() as i32)
                        .collect()
                })
                .collect();

            let result = gen_video_code_v0(&frame_sigs, bits)
                .unwrap_or_else(|e| panic!("gen_video_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 3, "expected 3 conformance tests to run");
    }

    #[test]
    fn test_gen_mixed_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_mixed_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let codes_json = inputs[0].as_array().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
            let expected_parts: Vec<&str> = tc["outputs"]["parts"]
                .as_array()
                .unwrap()
                .iter()
                .map(|v| v.as_str().unwrap())
                .collect();

            let codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();

            let result = gen_mixed_code_v0(&codes, bits)
                .unwrap_or_else(|e| panic!("gen_mixed_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            // Verify parts from struct match expected
            let result_parts: Vec<&str> = result.parts.iter().map(|s| s.as_str()).collect();
            assert_eq!(
                result_parts, expected_parts,
                "parts mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 2, "expected 2 conformance tests to run");
    }

    #[test]
    fn test_gen_mixed_code_v0_too_few_codes() {
        assert!(matches!(
            gen_mixed_code_v0(&["EUA6GIKXN42IQV3S"], 64),
            Err(IsccError::InvalidInput(_))
        ));
    }

    /// Build raw Content-Code bytes (header + body) for a given bit length.
    fn make_content_code_raw(stype: codec::SubType, bit_length: u32) -> Vec<u8> {
        let nbytes = (bit_length / 8) as usize;
        let body: Vec<u8> = (0..nbytes).map(|i| (i & 0xFF) as u8).collect();
        let base32 = codec::encode_component(
            codec::MainType::Content,
            stype,
            codec::Version::V0,
            bit_length,
            &body,
        )
        .unwrap();
        codec::decode_base32(&base32).unwrap()
    }

    #[test]
    fn test_soft_hash_codes_v0_rejects_short_code() {
        // One code with 64 bits, one with only 32 bits — should reject when requesting 64
        let code_64 = make_content_code_raw(codec::SubType::None, 64);
        let code_32 = make_content_code_raw(codec::SubType::Image, 32);
        let result = soft_hash_codes_v0(&[code_64, code_32], 64);
        assert!(
            matches!(&result, Err(IsccError::InvalidInput(msg)) if msg.contains("too short")),
            "expected InvalidInput with 'too short', got {result:?}"
        );
    }

    #[test]
    fn test_soft_hash_codes_v0_accepts_exact_length() {
        // Two codes with exactly 64 bits each — should succeed when requesting 64
        let code_a = make_content_code_raw(codec::SubType::None, 64);
        let code_b = make_content_code_raw(codec::SubType::Image, 64);
        let result = soft_hash_codes_v0(&[code_a, code_b], 64);
        assert!(result.is_ok(), "expected Ok, got {result:?}");
    }

    #[test]
    fn test_soft_hash_codes_v0_accepts_longer_codes() {
        // Two codes with 128 bits each — should succeed when requesting 64
        let code_a = make_content_code_raw(codec::SubType::None, 128);
        let code_b = make_content_code_raw(codec::SubType::Audio, 128);
        let result = soft_hash_codes_v0(&[code_a, code_b], 64);
        assert!(result.is_ok(), "expected Ok, got {result:?}");
    }

    #[test]
    fn test_gen_data_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_data_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let stream_str = inputs[0].as_str().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();

            // Parse "stream:" prefix — remainder is hex-encoded bytes
            let hex_data = stream_str
                .strip_prefix("stream:")
                .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {tc_name}"));
            let input_bytes = hex::decode(hex_data)
                .unwrap_or_else(|e| panic!("invalid hex in test case {tc_name}: {e}"));

            let result = gen_data_code_v0(&input_bytes, bits)
                .unwrap_or_else(|e| panic!("gen_data_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 4, "expected 4 conformance tests to run");
    }

    #[test]
    fn test_gen_instance_code_v0_empty() {
        let result = gen_instance_code_v0(b"", 64).unwrap();
        assert_eq!(result.iscc, "ISCC:IAA26E2JXH27TING");
        assert_eq!(result.filesize, 0);
        assert_eq!(
            result.datahash,
            "1e20af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
        );
    }

    #[test]
    fn test_gen_instance_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_instance_code_v0"];
        let cases = section.as_object().unwrap();

        for (name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let stream_str = inputs[0].as_str().unwrap();
            let bits = inputs[1].as_u64().unwrap() as u32;
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();

            // Parse "stream:" prefix — remainder is hex-encoded bytes
            let hex_data = stream_str
                .strip_prefix("stream:")
                .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {name}"));
            let input_bytes = hex::decode(hex_data)
                .unwrap_or_else(|e| panic!("invalid hex in test case {name}: {e}"));

            let result = gen_instance_code_v0(&input_bytes, bits)
                .unwrap_or_else(|e| panic!("gen_instance_code_v0 failed for {name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {name}"
            );

            // Verify datahash from struct
            if let Some(expected_datahash) = tc["outputs"].get("datahash") {
                let expected_datahash = expected_datahash.as_str().unwrap();
                assert_eq!(
                    result.datahash, expected_datahash,
                    "datahash mismatch in test case {name}"
                );
            }

            // Verify filesize from struct
            if let Some(expected_filesize) = tc["outputs"].get("filesize") {
                let expected_filesize = expected_filesize.as_u64().unwrap();
                assert_eq!(
                    result.filesize, expected_filesize,
                    "filesize mismatch in test case {name}"
                );
            }

            // Also verify filesize matches input data length
            assert_eq!(
                result.filesize,
                input_bytes.len() as u64,
                "filesize should match input length in test case {name}"
            );
        }
    }

    #[test]
    fn test_gen_iscc_code_v0_conformance() {
        let json_str = include_str!("../tests/data.json");
        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
        let section = &data["gen_iscc_code_v0"];
        let cases = section.as_object().unwrap();

        let mut tested = 0;

        for (tc_name, tc) in cases {
            let inputs = tc["inputs"].as_array().unwrap();
            let codes_json = inputs[0].as_array().unwrap();
            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();

            let codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();

            let result = gen_iscc_code_v0(&codes, false)
                .unwrap_or_else(|e| panic!("gen_iscc_code_v0 failed for {tc_name}: {e}"));
            assert_eq!(
                result.iscc, expected_iscc,
                "ISCC mismatch in test case {tc_name}"
            );

            tested += 1;
        }

        assert_eq!(tested, 5, "expected 5 conformance tests to run");
    }

    #[test]
    fn test_gen_iscc_code_v0_too_few_codes() {
        assert!(matches!(
            gen_iscc_code_v0(&["AAAWKLHFPV6OPKDG"], false),
            Err(IsccError::InvalidInput(_))
        ));
    }

    #[test]
    fn test_gen_iscc_code_v0_missing_instance() {
        // Two Meta codes — missing Data and Instance
        assert!(matches!(
            gen_iscc_code_v0(&["AAAWKLHFPV6OPKDG", "AAAWKLHFPV6OPKDG"], false),
            Err(IsccError::InvalidInput(_))
        ));
    }

    #[test]
    fn test_gen_iscc_code_v0_short_code() {
        // Code too short (< 16 chars)
        assert!(matches!(
            gen_iscc_code_v0(&["AAAWKLHFPV6", "AAAWKLHFPV6OPKDG"], false),
            Err(IsccError::InvalidInput(_))
        ));
    }

    /// Verify that a Data-URL with empty base64 payload enters the meta bytes path.
    ///
    /// Python reference: `if meta:` is truthy for `"data:application/json;base64,"` (non-empty
    /// string), so it enters the meta branch with `payload = b""`. The result must have
    /// `meta = Some(...)` containing the original Data-URL and `metahash` equal to
    /// `multi_hash_blake3(&[])` (BLAKE3 of empty bytes).
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_empty_data_url_enters_meta_branch() {
        let result =
            gen_meta_code_v0("Test", None, Some("data:application/json;base64,"), 64).unwrap();

        // Result should be Ok
        assert_eq!(result.name, "Test");

        // meta should contain the original Data-URL string (not None)
        assert_eq!(
            result.meta,
            Some("data:application/json;base64,".to_string()),
            "empty Data-URL payload should still enter meta branch"
        );

        // metahash should be BLAKE3 of empty bytes
        let expected_metahash = utils::multi_hash_blake3(&[]);
        assert_eq!(
            result.metahash, expected_metahash,
            "metahash should be BLAKE3 of empty bytes"
        );
    }

    /// Verify that `soft_hash_meta_v0_with_bytes` with empty bytes produces the same
    /// digest as `soft_hash_meta_v0` with no extra text.
    ///
    /// Python reference (`code_meta.py:142`): `if extra in {None, "", b""}:` returns
    /// name-only simhash without interleaving for all empty-like values.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_soft_hash_meta_v0_with_bytes_empty_equals_name_only() {
        let name_only = soft_hash_meta_v0("test", None);
        let empty_bytes = soft_hash_meta_v0_with_bytes("test", &[]);
        assert_eq!(
            name_only, empty_bytes,
            "empty bytes should produce same digest as name-only (no interleaving)"
        );
    }

    // ---- Algorithm constants tests ----

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_meta_trim_name_value() {
        assert_eq!(META_TRIM_NAME, 128);
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_meta_trim_description_value() {
        assert_eq!(META_TRIM_DESCRIPTION, 4096);
    }

    #[test]
    fn test_io_read_size_value() {
        assert_eq!(IO_READ_SIZE, 4_194_304);
    }

    #[test]
    fn test_text_ngram_size_value() {
        assert_eq!(TEXT_NGRAM_SIZE, 13);
    }

    // ---- encode_component Tier 1 wrapper tests ----

    /// Encode a known digest and verify the output matches the codec version.
    #[test]
    fn test_encode_component_matches_codec() {
        let digest = [0xABu8; 8];
        let tier1 = encode_component(3, 0, 0, 64, &digest).unwrap();
        let tier2 = codec::encode_component(
            codec::MainType::Data,
            codec::SubType::None,
            codec::Version::V0,
            64,
            &digest,
        )
        .unwrap();
        assert_eq!(tier1, tier2);
    }

    /// Round-trip: encode a digest and verify the result is a valid ISCC unit.
    #[test]
    fn test_encode_component_round_trip() {
        let digest = [0x42u8; 32];
        let result = encode_component(0, 0, 0, 64, &digest).unwrap();
        // Meta-Code with 64-bit digest should start with "AA"
        assert!(!result.is_empty());
    }

    /// Reject MainType::Iscc (value 5).
    #[test]
    fn test_encode_component_rejects_iscc() {
        let result = encode_component(5, 0, 0, 64, &[0u8; 8]);
        assert!(result.is_err());
    }

    /// Reject digest shorter than bit_length / 8.
    #[test]
    fn test_encode_component_rejects_short_digest() {
        let result = encode_component(0, 0, 0, 64, &[0u8; 4]);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("digest length 4 < bit_length/8 (8)"),
            "unexpected error: {err}"
        );
    }

    /// Reject invalid MainType value.
    #[test]
    fn test_encode_component_rejects_invalid_mtype() {
        let result = encode_component(99, 0, 0, 64, &[0u8; 8]);
        assert!(result.is_err());
    }

    /// Reject invalid SubType value.
    #[test]
    fn test_encode_component_rejects_invalid_stype() {
        let result = encode_component(0, 99, 0, 64, &[0u8; 8]);
        assert!(result.is_err());
    }

    /// Reject invalid Version value.
    #[test]
    fn test_encode_component_rejects_invalid_version() {
        let result = encode_component(0, 0, 99, 64, &[0u8; 8]);
        assert!(result.is_err());
    }

    // ---- iscc_decode tests ----

    /// Round-trip: encode a Meta-Code digest, decode back, verify all fields match.
    #[test]
    fn test_iscc_decode_round_trip_meta() {
        let digest = [0xaa_u8; 8];
        let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
        let (mt, st, vs, li, decoded_digest) = iscc_decode(&encoded).unwrap();
        assert_eq!(mt, 0, "MainType::Meta");
        assert_eq!(st, 0, "SubType::None");
        assert_eq!(vs, 0, "Version::V0");
        // encode_length(Meta, 64) → 64/32 - 1 = 1
        assert_eq!(li, 1, "length_index");
        assert_eq!(decoded_digest, digest.to_vec());
    }

    /// Round-trip with Content-Code (MainType=2, SubType::TEXT=0).
    #[test]
    fn test_iscc_decode_round_trip_content() {
        let digest = [0xbb_u8; 8];
        let encoded = encode_component(2, 0, 0, 64, &digest).unwrap();
        let (mt, st, vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
        assert_eq!(mt, 2, "MainType::Content");
        assert_eq!(st, 0, "SubType::TEXT");
        assert_eq!(vs, 0, "Version::V0");
        assert_eq!(decoded_digest, digest.to_vec());
    }

    /// Round-trip with Data-Code (MainType=3).
    #[test]
    fn test_iscc_decode_round_trip_data() {
        let digest = [0xcc_u8; 8];
        let encoded = encode_component(3, 0, 0, 64, &digest).unwrap();
        let (mt, _st, _vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
        assert_eq!(mt, 3, "MainType::Data");
        assert_eq!(decoded_digest, digest.to_vec());
    }

    /// Round-trip with Instance-Code (MainType=4).
    #[test]
    fn test_iscc_decode_round_trip_instance() {
        let digest = [0xdd_u8; 8];
        let encoded = encode_component(4, 0, 0, 64, &digest).unwrap();
        let (mt, _st, _vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
        assert_eq!(mt, 4, "MainType::Instance");
        assert_eq!(decoded_digest, digest.to_vec());
    }

    /// Decode with "ISCC:" prefix produces the same result.
    #[test]
    fn test_iscc_decode_with_prefix() {
        let digest = [0xaa_u8; 8];
        let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
        let with_prefix = format!("ISCC:{encoded}");
        let (mt, st, vs, li, decoded_digest) = iscc_decode(&with_prefix).unwrap();
        assert_eq!(mt, 0);
        assert_eq!(st, 0);
        assert_eq!(vs, 0);
        assert_eq!(li, 1);
        assert_eq!(decoded_digest, digest.to_vec());
    }

    /// Decode with dashes inserted in the string.
    #[test]
    fn test_iscc_decode_with_dashes() {
        let digest = [0xaa_u8; 8];
        let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
        // Insert dashes at arbitrary positions
        let with_dashes = format!("{}-{}-{}", &encoded[..4], &encoded[4..8], &encoded[8..]);
        let (mt, st, vs, li, decoded_digest) = iscc_decode(&with_dashes).unwrap();
        assert_eq!(mt, 0);
        assert_eq!(st, 0);
        assert_eq!(vs, 0);
        assert_eq!(li, 1);
        assert_eq!(decoded_digest, digest.to_vec());
    }

    /// Error on invalid base32 characters.
    #[test]
    fn test_iscc_decode_invalid_base32() {
        let result = iscc_decode("!!!INVALID!!!");
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("base32"), "expected base32 error: {err}");
    }

    /// Known value from conformance vectors: Meta-Code "ISCC:AAAZXZ6OU74YAZIM".
    /// MainType=Meta(0), SubType=None(0), Version=V0(0), 64-bit digest.
    #[test]
    fn test_iscc_decode_known_meta_code() {
        let (mt, st, vs, li, digest) = iscc_decode("ISCC:AAAZXZ6OU74YAZIM").unwrap();
        assert_eq!(mt, 0, "MainType::Meta");
        assert_eq!(st, 0, "SubType::None");
        assert_eq!(vs, 0, "Version::V0");
        assert_eq!(li, 1, "length_index for 64-bit");
        assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
    }

    /// Known value from conformance vectors: Instance-Code "ISCC:IAA26E2JXH27TING".
    /// MainType=Instance(4), SubType=None(0), Version=V0(0), 64-bit digest.
    #[test]
    fn test_iscc_decode_known_instance_code() {
        let (mt, st, vs, li, digest) = iscc_decode("ISCC:IAA26E2JXH27TING").unwrap();
        assert_eq!(mt, 4, "MainType::Instance");
        assert_eq!(st, 0, "SubType::None");
        assert_eq!(vs, 0, "Version::V0");
        assert_eq!(li, 1, "length_index for 64-bit");
        assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
    }

    /// Known value: Data-Code "ISCC:GAAXL2XYM5BQIAZ3".
    /// MainType=Data(3), SubType=None(0), Version=V0(0), 64-bit digest.
    #[test]
    fn test_iscc_decode_known_data_code() {
        let (mt, st, vs, _li, digest) = iscc_decode("ISCC:GAAXL2XYM5BQIAZ3").unwrap();
        assert_eq!(mt, 3, "MainType::Data");
        assert_eq!(st, 0, "SubType::None");
        assert_eq!(vs, 0, "Version::V0");
        assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
    }

    /// Verification criterion: round-trip with specific known values.
    /// encode_component(0, 0, 0, 64, &[0xaa;8]) → iscc_decode → (0, 0, 0, 1, vec![0xaa;8])
    #[test]
    fn test_iscc_decode_verification_round_trip() {
        let digest = [0xaa_u8; 8];
        let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
        let result = iscc_decode(&encoded).unwrap();
        assert_eq!(result, (0, 0, 0, 1, vec![0xaa; 8]));
    }

    /// Error on truncated input where body is shorter than expected digest length.
    #[test]
    fn test_iscc_decode_truncated_input() {
        // Encode a valid 256-bit Meta-Code, then truncate the base32 string
        let digest = [0xff_u8; 32];
        let encoded = encode_component(0, 0, 0, 256, &digest).unwrap();
        // Truncate to just the header portion (first few chars)
        let truncated = &encoded[..6];
        let result = iscc_decode(truncated);
        assert!(result.is_err(), "should fail on truncated input");
    }

    // --- json_to_data_url tests ---

    /// Basic JSON object produces a data URL with application/json media type.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_json_to_data_url_basic() {
        let url = json_to_data_url(r#"{"key": "value"}"#).unwrap();
        assert!(
            url.starts_with("data:application/json;base64,"),
            "expected application/json prefix, got: {url}"
        );
    }

    /// JSON with `@context` key uses application/ld+json media type.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_json_to_data_url_ld_json() {
        let url = json_to_data_url(r#"{"@context": "https://schema.org"}"#).unwrap();
        assert!(
            url.starts_with("data:application/ld+json;base64,"),
            "expected application/ld+json prefix, got: {url}"
        );
    }

    /// JCS canonicalization reorders keys alphabetically.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_json_to_data_url_jcs_ordering() {
        let url = json_to_data_url(r#"{"b":1,"a":2}"#).unwrap();
        // Extract and decode the base64 payload
        let b64 = url.split_once(',').unwrap().1;
        let decoded = data_encoding::BASE64.decode(b64.as_bytes()).unwrap();
        let canonical = std::str::from_utf8(&decoded).unwrap();
        assert_eq!(canonical, r#"{"a":2,"b":1}"#, "JCS should sort keys");
    }

    /// Round-trip: json_to_data_url output fed into decode_data_url recovers
    /// the JCS-canonical bytes.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_json_to_data_url_round_trip() {
        let input = r#"{"hello": "world", "num": 42}"#;
        let url = json_to_data_url(input).unwrap();
        let decoded_bytes = decode_data_url(&url).unwrap();
        // The decoded bytes should be JCS-canonical JSON
        let canonical: serde_json::Value =
            serde_json::from_slice(&decoded_bytes).expect("decoded bytes should be valid JSON");
        let original: serde_json::Value = serde_json::from_str(input).unwrap();
        assert_eq!(canonical, original, "round-trip preserves JSON semantics");
    }

    /// Invalid JSON string returns an error.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_json_to_data_url_invalid_json() {
        let result = json_to_data_url("not json");
        assert!(result.is_err(), "should reject invalid JSON");
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("invalid JSON"),
            "expected 'invalid JSON' in error: {err}"
        );
    }

    /// Compatibility with conformance vector test_0016_meta_data_url.
    ///
    /// The conformance vector's meta field is:
    ///   data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9
    /// which encodes `{"some": "object"}` (with space after colon).
    ///
    /// Our function differs in two ways:
    /// 1. No `charset=utf-8` parameter (matching Python's DataURL.from_byte_data)
    /// 2. JCS canonicalization removes whitespace: `{"some":"object"}` (no space)
    ///
    /// We verify: (a) correct media type prefix, and (b) decoded payload equals
    /// JCS-canonical form of the same JSON input.
    #[cfg(feature = "meta-code")]
    #[test]
    fn test_json_to_data_url_conformance_0016() {
        let url = json_to_data_url(r#"{"some": "object"}"#).unwrap();
        // (a) Correct media type prefix (no charset, no @context → application/json)
        assert!(
            url.starts_with("data:application/json;base64,"),
            "expected application/json prefix"
        );
        // (b) Decoded payload is JCS-canonical (no whitespace)
        let b64 = url.split_once(',').unwrap().1;
        let decoded = data_encoding::BASE64.decode(b64.as_bytes()).unwrap();
        let canonical = std::str::from_utf8(&decoded).unwrap();
        assert_eq!(
            canonical, r#"{"some":"object"}"#,
            "JCS removes whitespace from JSON"
        );
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_meta_trim_meta_value() {
        assert_eq!(META_TRIM_META, 128_000);
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_meta_at_limit() {
        // Create a JSON payload that decodes to exactly 128,000 bytes
        // JSON: {"x":"<padding>"} where padding fills to 128,000 bytes
        // The canonical JSON overhead is {"x":""} = 8 bytes, so padding = 127,992 bytes
        let padding = "a".repeat(128_000 - 8);
        let json_str = format!(r#"{{"x":"{padding}"}}"#);
        let result = gen_meta_code_v0("test", None, Some(&json_str), 64);
        assert!(
            result.is_ok(),
            "payload at exactly META_TRIM_META should succeed"
        );
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_meta_over_limit() {
        // Create a JSON payload that decodes to 128,001 bytes (one over limit)
        let padding = "a".repeat(128_000 - 8 + 1);
        let json_str = format!(r#"{{"x":"{padding}"}}"#);
        let result = gen_meta_code_v0("test", None, Some(&json_str), 64);
        assert!(
            matches!(result, Err(IsccError::InvalidInput(ref msg)) if msg.contains("size limit")),
            "payload exceeding META_TRIM_META should return InvalidInput"
        );
    }

    #[cfg(feature = "meta-code")]
    #[test]
    fn test_gen_meta_code_v0_data_url_pre_decode_reject() {
        // Create a Data-URL string exceeding the pre-decode limit
        // PRE_DECODE_LIMIT = META_TRIM_META * 4 / 3 + 256 = 170,922
        let pre_decode_limit = META_TRIM_META * 4 / 3 + 256;
        let padding = "A".repeat(pre_decode_limit + 1);
        let data_url = format!("data:application/octet-stream;base64,{padding}");
        let result = gen_meta_code_v0("test", None, Some(&data_url), 64);
        assert!(
            matches!(result, Err(IsccError::InvalidInput(ref msg)) if msg.contains("size limit")),
            "oversized Data-URL should be rejected before decoding"
        );
    }

    // ---- gen_sum_code_v0 tests ----

    /// Helper: write data to a unique temp file and return the path.
    fn write_temp_file(name: &str, data: &[u8]) -> std::path::PathBuf {
        let path = std::env::temp_dir().join(format!("iscc_test_{name}"));
        std::fs::write(&path, data).expect("failed to write temp file");
        path
    }

    #[test]
    fn test_gen_sum_code_v0_equivalence() {
        let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0.";
        let path = write_temp_file("sum_equiv", data);

        let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();

        // Compute the same result via separate functions
        let data_result = gen_data_code_v0(data, 64).unwrap();
        let instance_result = gen_instance_code_v0(data, 64).unwrap();
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();

        assert_eq!(sum_result.iscc, iscc_result.iscc);
        assert_eq!(sum_result.datahash, instance_result.datahash);
        assert_eq!(sum_result.filesize, instance_result.filesize);
        assert_eq!(sum_result.filesize, data.len() as u64);
        assert_eq!(sum_result.units, None);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_empty_file() {
        let path = write_temp_file("sum_empty", b"");

        let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();

        let data_result = gen_data_code_v0(b"", 64).unwrap();
        let instance_result = gen_instance_code_v0(b"", 64).unwrap();
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();

        assert_eq!(sum_result.iscc, iscc_result.iscc);
        assert_eq!(sum_result.datahash, instance_result.datahash);
        assert_eq!(sum_result.filesize, 0);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_file_not_found() {
        let path = std::env::temp_dir().join("iscc_test_nonexistent_file_xyz");
        let result = gen_sum_code_v0(&path, 64, false, false);
        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("Cannot open file"),
            "error message should mention file open failure: {err_msg}"
        );
    }

    #[test]
    fn test_gen_sum_code_v0_wide_mode() {
        let data = b"Testing wide mode for gen_sum_code_v0 function.";
        let path = write_temp_file("sum_wide", data);

        let narrow = gen_sum_code_v0(&path, 64, false, false).unwrap();
        let wide = gen_sum_code_v0(&path, 64, true, false).unwrap();

        // Wide mode with 64-bit codes doesn't trigger (need 128+), so they should be equal
        assert_eq!(narrow.iscc, wide.iscc);

        // With 128 bits, wide mode should produce a different (longer) ISCC
        let narrow_128 = gen_sum_code_v0(&path, 128, false, false).unwrap();
        let wide_128 = gen_sum_code_v0(&path, 128, true, false).unwrap();
        assert_ne!(narrow_128.iscc, wide_128.iscc);

        // Both should have the same datahash and filesize
        assert_eq!(narrow_128.datahash, wide_128.datahash);
        assert_eq!(narrow_128.filesize, wide_128.filesize);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_bits_64() {
        let data = b"Testing 64-bit gen_sum_code_v0.";
        let path = write_temp_file("sum_bits64", data);

        let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();

        let data_result = gen_data_code_v0(data, 64).unwrap();
        let instance_result = gen_instance_code_v0(data, 64).unwrap();
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();

        assert_eq!(sum_result.iscc, iscc_result.iscc);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_bits_128() {
        let data = b"Testing 128-bit gen_sum_code_v0.";
        let path = write_temp_file("sum_bits128", data);

        let sum_result = gen_sum_code_v0(&path, 128, false, false).unwrap();

        let data_result = gen_data_code_v0(data, 128).unwrap();
        let instance_result = gen_instance_code_v0(data, 128).unwrap();
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();

        assert_eq!(sum_result.iscc, iscc_result.iscc);
        assert_eq!(sum_result.datahash, instance_result.datahash);
        assert_eq!(sum_result.filesize, data.len() as u64);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_large_data() {
        // Generate data large enough to produce multiple CDC chunks
        let data: Vec<u8> = (0..50_000).map(|i| (i % 256) as u8).collect();
        let path = write_temp_file("sum_large", &data);

        let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();

        let data_result = gen_data_code_v0(&data, 64).unwrap();
        let instance_result = gen_instance_code_v0(&data, 64).unwrap();
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();

        assert_eq!(sum_result.iscc, iscc_result.iscc);
        assert_eq!(sum_result.datahash, instance_result.datahash);
        assert_eq!(sum_result.filesize, data.len() as u64);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_units_enabled() {
        let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0 units.";
        let path = write_temp_file("sum_units_on", data);

        let sum_result = gen_sum_code_v0(&path, 64, false, true).unwrap();

        // units should be Some with exactly 2 elements
        let units = sum_result.units.as_ref().expect("units should be Some");
        assert_eq!(
            units.len(),
            2,
            "units should contain [Data-Code, Instance-Code]"
        );

        // First unit should be a Data-Code (MainType::Data = 3)
        let (maintype, ..) = iscc_decode(&units[0]).unwrap();
        assert_eq!(
            maintype, 3,
            "first unit should be a Data-Code (MainType::Data = 3)"
        );

        // Second unit should be an Instance-Code (MainType::Instance = 4)
        let (maintype, ..) = iscc_decode(&units[1]).unwrap();
        assert_eq!(
            maintype, 4,
            "second unit should be an Instance-Code (MainType::Instance = 4)"
        );

        // Units should match individually computed codes
        let data_result = gen_data_code_v0(data, 64).unwrap();
        let instance_result = gen_instance_code_v0(data, 64).unwrap();
        assert_eq!(units[0], data_result.iscc);
        assert_eq!(units[1], instance_result.iscc);

        // The composite ISCC should still be correct
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
        assert_eq!(sum_result.iscc, iscc_result.iscc);

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_gen_sum_code_v0_units_disabled() {
        let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0 no units.";
        let path = write_temp_file("sum_units_off", data);

        let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();

        assert_eq!(
            sum_result.units, None,
            "units should be None when add_units is false"
        );

        // The composite ISCC should still be correct
        let data_result = gen_data_code_v0(data, 64).unwrap();
        let instance_result = gen_instance_code_v0(data, 64).unwrap();
        let iscc_result =
            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
        assert_eq!(sum_result.iscc, iscc_result.iscc);

        std::fs::remove_file(&path).ok();
    }
}
