use serde::{Deserialize, Serialize};

/// Stable identity for the exact bytes of an imported project source.
///
/// Filesystem timestamps are deliberately excluded: Git and LFS do not preserve checkout mtimes,
/// so they cannot participate in a portable authored-data contract.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AssetSourceFingerprint {
    pub byte_len: u64,
    #[serde(default)]
    pub content_hash: String,
}

impl AssetSourceFingerprint {
    pub fn from_bytes(bytes: &[u8]) -> Self {
        Self {
            byte_len: bytes.len() as u64,
            content_hash: blake3::hash(bytes).to_hex().to_string(),
        }
    }

    pub fn is_complete(&self) -> bool {
        self.content_hash.len() == 64
            && self
                .content_hash
                .bytes()
                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    }
}

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

    #[test]
    fn exact_bytes_define_source_identity() {
        let first = AssetSourceFingerprint::from_bytes(b"asset-a");
        let same = AssetSourceFingerprint::from_bytes(b"asset-a");
        let same_size_change = AssetSourceFingerprint::from_bytes(b"asset-b");

        assert_eq!(first, same);
        assert_ne!(first, same_size_change);
        assert_eq!(first.byte_len, same_size_change.byte_len);
        assert!(first.is_complete());

        let mut noncanonical = first;
        noncanonical.content_hash.make_ascii_uppercase();
        assert!(!noncanonical.is_complete());
    }
}
