//! jwp-receipts §2.1: the content hash of a delivered object.
//!
//! `content_hash = BLAKE3("moq-jwp:object\x01" ‖ envelope)`, over the envelope bytes
//! exactly as they travel. It is an object's identity in a JWP object receipt, the
//! subject of a claim about that object, and the leaf a labelled delivery receipt
//! commits to. This module holds it alone, gated on nothing but BLAKE3, so every
//! receipt family that names an object computes it in one place.

/// jwp-receipts §2.1: the per-object content hash domain.
pub const DOMAIN_OBJECT: &[u8] = b"moq-jwp:object\x01";

/// jwp-receipts §2.1 content hash of a WAI envelope: BLAKE3 over [`DOMAIN_OBJECT`]
/// followed by the envelope bytes.
pub fn content_hash(envelope_bytes: &[u8]) -> [u8; 32] {
    let mut h = blake3::Hasher::new();
    h.update(DOMAIN_OBJECT);
    h.update(envelope_bytes);
    *h.finalize().as_bytes()
}

/// How a verifier came to hold the content hash it compares an object receipt's with
/// (jwp-receipts §3 step 4).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Establishment {
    /// Recomputed over the envelope bytes the verifier received.
    Envelope,
    /// The cid a verified head proof establishes, for an object the verifier received
    /// only in part over HTTP, each component it uses checked against its entry's
    /// digest (http-delivery §6, §7, §8.3).
    HeadProof,
}

impl Establishment {
    /// `envelope` or `head-proof`.
    pub fn label(self) -> &'static str {
        match self {
            Establishment::Envelope => "envelope",
            Establishment::HeadProof => "head-proof",
        }
    }
}

/// The content hash jwp-receipts §3 step 4 compares an object receipt's `content_hash`
/// with, and how the verifier came to hold it. It is built only from bytes — the
/// envelope, hashed here ([`EstablishedHash::of_envelope`]) — or by an HTTP fetch whose
/// head proof verified against the cid and whose every used component matched its
/// entry's digest (`http_binding::session::FetchSession::established`). A value a receipt
/// or an index states never becomes one, so a check over it cannot pass on the receipt's
/// own word.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EstablishedHash {
    hash: [u8; 32],
    by: Establishment,
}

impl EstablishedHash {
    /// The content hash recomputed over `envelope`, the bytes as received.
    pub fn of_envelope(envelope: &[u8]) -> EstablishedHash {
        EstablishedHash { hash: content_hash(envelope), by: Establishment::Envelope }
    }

    /// The cid a head proof verified, for an object received in part
    /// (http-delivery §8.3). Built only by a fetch session that verified it.
    #[cfg_attr(not(feature = "http"), allow(dead_code))]
    pub(crate) fn by_head_proof(cid: [u8; 32]) -> EstablishedHash {
        EstablishedHash { hash: cid, by: Establishment::HeadProof }
    }

    /// The content hash.
    pub fn hash(&self) -> &[u8; 32] {
        &self.hash
    }

    /// How it was established.
    pub fn by(&self) -> Establishment {
        self.by
    }
}

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

    /// The known answer claim-conformance's independent verifier computes for its
    /// canonical fixture object (`claim_verify.py`, BLAKE3 over the domain and the
    /// object), so the hoisted function is the one the corpus froze.
    #[test]
    fn content_hash_known_answer() {
        let h = content_hash(b"claim-conformance: canonical fixture object");
        let hex: String = h.iter().map(|b| format!("{b:02x}")).collect();
        assert_eq!(hex, "af904358ba08fb542a845ce37edb3e5ee7f56eb362dfb1ee10b209e167d420de");
    }

    /// An established hash over bytes is the content hash of those bytes, marked as
    /// recomputed: the one public way to build one.
    #[test]
    fn an_established_hash_is_recomputed_from_bytes() {
        let e = EstablishedHash::of_envelope(b"claim-conformance: canonical fixture object");
        assert_eq!(*e.hash(), content_hash(b"claim-conformance: canonical fixture object"));
        assert_eq!(e.by(), Establishment::Envelope);
        assert_eq!(EstablishedHash::by_head_proof([7; 32]).by().label(), "head-proof");
    }
}
