//! Pluggable blob-storage backend trait (D23).
//!
//! The runtime is parametric over any type implementing [`BlobBackend`].
//! [`LocalFilesystemBlobs`](crate::store::LocalFilesystemBlobs) is the
//! default impl shipped with the substrate — content-addressed files on
//! the local disk, no external dependencies. Alternative backends
//! (`S3Blobs`, `OciBlobs`) are placeholder modules behind feature flags;
//! their real implementations are deferred per the trigger-based
//! discipline (D23).

use crate::error::Result;
use crate::store::BlobId;

/// Pluggable content-addressed blob store.
///
/// Contract any backend MUST honour:
///
/// 1. `put(bytes)` returns the address `BlobId::from_bytes(bytes)` — addressing
///    is by blake3, identical across backends.
/// 2. `get(id)` returns the exact bytes that produced `id`, or an error.
///    Corrupted bytes MUST be rejected with [`crate::Error::Corrupt`].
/// 3. `exists(id)` is consistent with `get(id)`: it returns true iff a
///    subsequent `get(id)` would not return `NotFound`. It MUST NOT perform an
///    integrity check (use `get` for that).
/// 4. `list_prefix(prefix_hex)` returns every id whose hex representation
///    starts with `prefix_hex`. An empty `prefix_hex` returns every id known to
///    the backend.
pub trait BlobBackend {
    /// Store `bytes` and return its content address.
    ///
    /// # Errors
    ///
    /// Backend-specific. The local-filesystem default returns IO errors
    /// on write failure.
    fn put(&self, bytes: &[u8]) -> Result<BlobId>;

    /// Retrieve bytes by content address.
    ///
    /// # Errors
    ///
    /// - [`crate::Error::NotFound`] when the blob is absent.
    /// - [`crate::Error::Corrupt`] when stored bytes do not hash to `id`.
    /// - [`crate::Error::Io`] on transport failure.
    fn get(&self, id: &BlobId) -> Result<Vec<u8>>;

    /// Whether `id` is reachable (without integrity check).
    fn exists(&self, id: &BlobId) -> bool;

    /// List every known id whose hex representation starts with
    /// `prefix_hex`.
    ///
    /// # Errors
    ///
    /// Backend-specific IO errors.
    fn list_prefix(&self, prefix_hex: &str) -> Result<Vec<BlobId>>;
}

#[cfg(test)]
#[test]
fn local_filesystem() {
    use crate::store::LocalFilesystemBlobs;

    let tmp = tempfile::tempdir().expect("tempdir");
    let backend: LocalFilesystemBlobs =
        LocalFilesystemBlobs::open(tmp.path()).expect("open backend");

    // put / get round trip
    let bytes = b"hello blobs".to_vec();
    let id = BlobBackend::put(&backend, &bytes).expect("put ok");
    let fetched = BlobBackend::get(&backend, &id).expect("get ok");
    assert_eq!(fetched, bytes);

    // exists is true after put, false for an unknown id
    assert!(BlobBackend::exists(&backend, &id));
    let unknown = BlobId::from_bytes(b"never stored");
    assert!(!BlobBackend::exists(&backend, &unknown));

    // put is idempotent
    let id_again = BlobBackend::put(&backend, &bytes).expect("re-put ok");
    assert_eq!(id, id_again);

    // list_prefix finds the blob by its hex prefix
    let hex = id.to_hex();
    let prefix = &hex[..4];
    let found = BlobBackend::list_prefix(&backend, prefix).expect("list ok");
    assert!(
        found.iter().any(|x| x == &id),
        "list_prefix({prefix}) must include the stored blob"
    );

    // empty prefix returns at least the stored blob
    let all = BlobBackend::list_prefix(&backend, "").expect("list all ok");
    assert!(all.iter().any(|x| x == &id));

    // unknown prefix returns empty
    let none = BlobBackend::list_prefix(&backend, "ffffffffff").expect("list ok");
    assert!(
        !none.iter().any(|x| x == &id),
        "id with a different prefix must not be returned"
    );
}
