//! Read-only Git object access behind local backend traits.

use crate::cache::{ByteLruCache, CacheWeight, TtlLruCache};
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fs::{self, File, OpenOptions};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::mem::size_of;
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use gix::bstr::ByteSlice;
use gix::hash::oid;
use gix::object::tree::EntryKind;

const DEFAULT_CACHE_SIZE_MB: usize = 256;
const DEFAULT_REVISION_CACHE_ENTRIES: usize = 256;
const DEFAULT_REVISION_CACHE_TTL: Duration = Duration::from_secs(1);
const DEFAULT_SMALL_BLOB_MAX_BYTES: usize = 128 * 1024;
const MAX_LFS_POINTER_BYTES: u64 = 8 * 1024;

thread_local! {
    static THREAD_LOCAL_REPOSITORIES: RefCell<HashMap<usize, gix::Repository>> =
        RefCell::new(HashMap::new());
}

static NEXT_REPOSITORY_KEY: AtomicUsize = AtomicUsize::new(1);

/// A boxed reader for blob content that can be streamed and seeked.
pub type BlobReader = Box<dyn ReadSeek + Send>;

/// A helper trait for trait-object-safe blob readers.
pub trait ReadSeek: Read + Seek {}

impl<T> ReadSeek for T where T: Read + Seek {}

/// The exposed state of a Git LFS pointer blob.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LfsState {
    /// The mounted file exposes the pointer blob itself.
    Pointer,
    /// The mounted file exposes the locally available LFS object content.
    Resolved,
}

/// A locatable submodule checkout opened as its own object store.
#[derive(Debug)]
pub struct OpenedSubmodule<S> {
    /// The opened submodule repository.
    pub store: S,
    /// The pinned commit resolved within the submodule repository.
    pub commit: CommitInfo,
    /// The root tree of `commit`.
    pub tree: TreeInfo,
}

/// Minimal revision resolution services needed by the filesystem.
pub trait RevResolver: Send + Sync {
    /// Resolve `HEAD` to the commit it currently points to.
    fn resolve_head(&self) -> Result<CommitInfo, StoreError>;

    /// Resolve an arbitrary revision spec to a commit.
    fn resolve_revision(&self, spec: &[u8]) -> Result<CommitInfo, StoreError>;

    /// Resolve a full or abbreviated commit hash to a commit.
    fn resolve_commit_prefix(&self, prefix: &[u8]) -> Result<CommitInfo, StoreError>;

    /// Enumerate browsable references in the repository.
    fn list_references(&self) -> Result<Vec<GitReference>, StoreError>;
}

/// Minimal read-only object access needed by the filesystem.
pub trait ObjectStore: Send + Sync {
    /// Open a repository for read-only access.
    fn open(path: &Path) -> Result<Self, StoreError>
    where
        Self: Sized;

    /// Obtain the root tree for the given commit.
    fn root_tree(&self, commit: &CommitInfo) -> Result<TreeInfo, StoreError>;

    /// List every direct entry in a tree.
    fn list_tree(&self, tree: &TreeInfo) -> Result<Vec<TreeEntry>, StoreError>;

    /// Return the uncompressed byte length of a blob.
    fn blob_len(&self, oid: &GitObjectId) -> Result<u64, StoreError>;

    /// Open a blob for streaming reads.
    fn open_blob(&self, oid: &GitObjectId) -> Result<BlobReader, StoreError>;

    /// Read the target bytes of a symlink entry.
    fn read_symlink_target(&self, oid: &GitObjectId) -> Result<Vec<u8>, StoreError>;

    /// Return the LFS exposure state for a pointer blob, if the blob is an LFS pointer.
    fn lfs_state(&self, oid: &GitObjectId) -> Result<Option<LfsState>, StoreError>;

    /// Open a submodule checkout at `path` and resolve the pinned `commit` when possible.
    fn open_submodule(
        &self,
        path: &[u8],
        commit: &GitObjectId,
    ) -> Result<Option<OpenedSubmodule<Self>>, StoreError>
    where
        Self: Sized;

    /// Return the commit-ordered history entries for a repo-relative path.
    fn history_entries(&self, path: &[u8]) -> Result<Vec<HistoryEntry>, StoreError>;

    /// Return `true` if `prefix` can lead to a deeper `history/` path.
    fn history_prefix_exists(&self, prefix: &[u8]) -> Result<bool, StoreError>;
}

/// The object id of a Git object, stored as lowercase hexadecimal.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GitObjectId(String);

impl GitObjectId {
    /// Return the lowercase hexadecimal representation of the object id.
    pub fn as_hex(&self) -> &str {
        &self.0
    }

    fn from_gix(oid: &oid) -> Self {
        Self(oid.to_hex().to_string())
    }

    fn to_gix(&self) -> Result<gix::ObjectId, StoreError> {
        gix::ObjectId::from_hex(self.0.as_bytes()).map_err(|source| StoreError::InvalidObjectId {
            hex: self.0.clone(),
            source,
        })
    }
}

impl std::fmt::Display for GitObjectId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

/// A resolved commit and the metadata timefs needs from it.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommitInfo {
    /// The commit object id.
    pub oid: GitObjectId,
    /// The commit's committer timestamp in whole seconds since the Unix epoch.
    pub committer_time_seconds: i64,
}

/// A Git tree object.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeInfo {
    /// The tree object id.
    pub oid: GitObjectId,
}

/// A repository reference that can be exposed in the filesystem namespace.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitReference {
    /// The full reference name, such as `refs/heads/main`.
    pub full_name: Vec<u8>,
}

/// A single commit-backed version of a file in `history/<path>/`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HistoryEntry {
    /// The commit containing this version.
    pub commit: CommitInfo,
    /// The repo-relative path to the file in that commit.
    pub path: Vec<u8>,
}

/// The Git object kind stored in a tree entry.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitObjectKind {
    /// A nested tree object.
    Tree,
    /// A blob object, including executables and symlinks.
    Blob,
    /// A gitlink entry pointing at a commit.
    Commit,
}

impl GitObjectKind {
    fn from_entry_kind(kind: EntryKind) -> Self {
        match kind {
            EntryKind::Tree => Self::Tree,
            EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link => Self::Blob,
            EntryKind::Commit => Self::Commit,
        }
    }
}

/// A single direct entry within a tree.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeEntry {
    /// The raw entry name bytes.
    pub name: Vec<u8>,
    /// The object id referenced by the entry.
    pub oid: GitObjectId,
    /// The Git file mode, e.g. `0o100644`.
    pub mode: u32,
    /// The Git object kind referenced by the entry.
    pub kind: GitObjectKind,
}

impl TreeEntry {
    fn approximate_size(&self) -> usize {
        size_of::<Self>() + self.name.len() + self.oid.as_hex().len()
    }
}

impl CacheWeight for Vec<TreeEntry> {
    fn cache_weight(&self) -> usize {
        size_of::<Vec<TreeEntry>>() + self.iter().map(TreeEntry::approximate_size).sum::<usize>()
    }
}

impl CacheWeight for Arc<[u8]> {
    fn cache_weight(&self) -> usize {
        size_of::<Arc<[u8]>>() + self.len()
    }
}

/// Errors produced by the read-only Git backend.
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
    /// Opening the repository failed.
    #[error("failed to open repository at {path}: {source}")]
    OpenRepository {
        /// The path that was opened.
        path: PathBuf,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Resolving `HEAD` failed.
    #[error("failed to resolve HEAD: {source}")]
    ResolveHead {
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Resolving a revision spec failed because it did not identify a commit.
    #[error("revision did not resolve to a commit: {spec}: {source}")]
    ResolveRevision {
        /// The revision text that was resolved.
        spec: String,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Resolving a revision spec failed because it does not exist.
    #[error("revision not found: {spec}: {source}")]
    RevisionNotFound {
        /// The revision text that was resolved.
        spec: String,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Resolving a revision spec failed because it is ambiguous.
    #[error("revision is ambiguous: {spec}: {source}")]
    AmbiguousRevision {
        /// The revision text that was resolved.
        spec: String,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Enumerating repository references failed.
    #[error("failed to enumerate references: {source}")]
    ListReferences {
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Reading a commit object failed.
    #[error("failed to read commit {oid}: {source}")]
    ReadCommit {
        /// The commit object id.
        oid: GitObjectId,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Reading a tree object failed.
    #[error("failed to read tree {oid}: {source}")]
    ReadTree {
        /// The tree object id.
        oid: GitObjectId,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Reading a blob object failed.
    #[error("failed to read blob {oid}: {source}")]
    ReadBlob {
        /// The blob object id.
        oid: GitObjectId,
        /// The underlying error from the Git backend.
        source: anyhow::Error,
    },

    /// Parsing a hexadecimal object id failed.
    #[error("invalid object id {hex}: {source}")]
    InvalidObjectId {
        /// The invalid hexadecimal text.
        hex: String,
        /// The parse failure.
        source: gix::hash::decode::Error,
    },

    /// Tree data could not be decoded.
    #[error("failed to decode tree {oid}: {source}")]
    DecodeTree {
        /// The tree object id.
        oid: GitObjectId,
        /// The decode failure.
        source: gix::objs::decode::Error,
    },

    /// Commit data could not be decoded.
    #[error("failed to decode commit {oid}: {source}")]
    DecodeCommit {
        /// The commit object id.
        oid: GitObjectId,
        /// The decode failure.
        source: gix::objs::decode::Error,
    },

    /// Enumerating or resolving file history failed.
    #[error("failed to resolve history for {path}: {source}")]
    HistoryLookup {
        /// The repo-relative path that was queried.
        path: String,
        /// The underlying failure.
        source: anyhow::Error,
    },
}

#[derive(Clone, Copy, Debug)]
struct CacheLayout {
    tree_cache_bytes: usize,
    blob_cache_bytes: usize,
    small_blob_threshold: usize,
    revision_cache_entries: usize,
    revision_ttl: Duration,
}

impl CacheLayout {
    fn from_cache_size_mb(cache_size_mb: usize) -> Self {
        let object_cache_bytes = cache_size_mb.saturating_mul(1024 * 1024);
        let tree_cache_bytes = object_cache_bytes / 2;
        let blob_cache_bytes = object_cache_bytes.saturating_sub(tree_cache_bytes);
        let small_blob_threshold = blob_cache_bytes.min(DEFAULT_SMALL_BLOB_MAX_BYTES);
        Self {
            tree_cache_bytes,
            blob_cache_bytes,
            small_blob_threshold,
            revision_cache_entries: DEFAULT_REVISION_CACHE_ENTRIES,
            revision_ttl: DEFAULT_REVISION_CACHE_TTL,
        }
    }
}

#[derive(Debug)]
struct RepositoryCaches {
    trees: Mutex<ByteLruCache<GitObjectId, Vec<TreeEntry>>>,
    small_blobs: Mutex<ByteLruCache<GitObjectId, Arc<[u8]>>>,
    revisions: Mutex<TtlLruCache<RevisionCacheKey, CommitInfo>>,
}

impl RepositoryCaches {
    fn new(layout: CacheLayout) -> Self {
        Self {
            trees: Mutex::new(ByteLruCache::new(layout.tree_cache_bytes)),
            small_blobs: Mutex::new(ByteLruCache::new(layout.blob_cache_bytes)),
            revisions: Mutex::new(TtlLruCache::new(
                layout.revision_cache_entries,
                layout.revision_ttl,
            )),
        }
    }
}

#[derive(Debug, Default)]
struct RepositoryMetrics {
    tree_cache_hits: AtomicU64,
    tree_cache_misses: AtomicU64,
    tree_backend_reads: AtomicU64,
    blob_cache_hits: AtomicU64,
    blob_cache_misses: AtomicU64,
    blob_backend_reads: AtomicU64,
    blob_streamed_opens: AtomicU64,
    revision_cache_hits: AtomicU64,
    revision_cache_misses: AtomicU64,
    revision_backend_resolves: AtomicU64,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum RevisionCacheKey {
    Head,
    Revision(Vec<u8>),
    CommitPrefix(Vec<u8>),
}

#[derive(Debug)]
struct TempBlobFile {
    file: File,
    path: PathBuf,
}

impl Read for TempBlobFile {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.file.read(buf)
    }
}

impl Seek for TempBlobFile {
    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
        self.file.seek(pos)
    }
}

impl Drop for TempBlobFile {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.path);
    }
}

#[derive(Debug)]
struct LfsPointer {
    oid: String,
}

#[derive(Debug)]
struct ResolvedLfsObject {
    path: PathBuf,
    size: u64,
}

#[derive(Clone, Debug)]
struct SnapshotRevision {
    name: Vec<u8>,
    commit: CommitInfo,
}

#[derive(Clone, Debug)]
struct ReferenceSnapshot {
    head: CommitInfo,
    references: Vec<GitReference>,
    revisions: Vec<SnapshotRevision>,
}

impl ReferenceSnapshot {
    fn resolve_named_commit(&self, name: &[u8]) -> Option<CommitInfo> {
        self.revisions
            .iter()
            .find(|revision| revision.name == name)
            .map(|revision| revision.commit.clone())
    }
}

/// A `gix`-backed implementation of the local backend traits.
#[derive(Debug)]
pub struct GixRepository {
    key: usize,
    repo: gix::ThreadSafeRepository,
    repo_path: PathBuf,
    git_dir: PathBuf,
    cache_layout: CacheLayout,
    caches: RepositoryCaches,
    metrics: RepositoryMetrics,
    temp_blob_counter: AtomicU64,
    handle_creations: AtomicUsize,
    lfs_enabled: bool,
    reference_snapshot: Option<ReferenceSnapshot>,
}

impl GixRepository {
    /// Open a repository with explicit backend options.
    pub fn open_with_options(
        path: &Path,
        cache_size_mb: usize,
        lfs_enabled: bool,
        ref_snapshot: bool,
    ) -> Result<Self, StoreError> {
        let repo = gix::ThreadSafeRepository::discover(path).map_err(|source| {
            StoreError::OpenRepository {
                path: path.to_path_buf(),
                source: anyhow::Error::new(source),
            }
        })?;
        Self::new(
            repo,
            CacheLayout::from_cache_size_mb(cache_size_mb),
            lfs_enabled,
            ref_snapshot,
        )
    }

    /// Open a repository with a cache budget expressed in MiB.
    pub fn open_with_cache_size_mb(path: &Path, cache_size_mb: usize) -> Result<Self, StoreError> {
        Self::open_with_options(path, cache_size_mb, false, false)
    }

    fn new(
        repo: gix::ThreadSafeRepository,
        cache_layout: CacheLayout,
        lfs_enabled: bool,
        ref_snapshot: bool,
    ) -> Result<Self, StoreError> {
        let repo_path = repo.work_dir().unwrap_or_else(|| repo.path()).to_path_buf();
        let git_dir = repo.path().to_path_buf();
        let mut store = Self {
            key: NEXT_REPOSITORY_KEY.fetch_add(1, Ordering::Relaxed),
            repo,
            repo_path,
            git_dir,
            cache_layout,
            caches: RepositoryCaches::new(cache_layout),
            metrics: RepositoryMetrics::default(),
            temp_blob_counter: AtomicU64::new(0),
            handle_creations: AtomicUsize::new(0),
            lfs_enabled,
            reference_snapshot: None,
        };
        if ref_snapshot {
            store.reference_snapshot = Some(store.capture_reference_snapshot()?);
        }
        Ok(store)
    }

    fn with_thread_local_repo<T, F>(&self, op: F) -> T
    where
        F: FnOnce(&gix::Repository) -> T,
    {
        THREAD_LOCAL_REPOSITORIES.with(|repositories| {
            let mut repositories = repositories.borrow_mut();
            let repo = repositories.entry(self.key).or_insert_with(|| {
                self.handle_creations.fetch_add(1, Ordering::Relaxed);
                self.repo.to_thread_local()
            });
            op(repo)
        })
    }

    fn cached_tree_entries(&self, tree: &TreeInfo) -> Result<Vec<TreeEntry>, StoreError> {
        {
            let mut cache = match self.caches.trees.lock() {
                Ok(cache) => cache,
                Err(poisoned) => poisoned.into_inner(),
            };
            if let Some(entries) = cache.get_cloned(&tree.oid) {
                self.metrics.tree_cache_hits.fetch_add(1, Ordering::Relaxed);
                return Ok(entries);
            }
        }

        self.metrics
            .tree_cache_misses
            .fetch_add(1, Ordering::Relaxed);
        self.metrics
            .tree_backend_reads
            .fetch_add(1, Ordering::Relaxed);

        let entries = self.with_thread_local_repo(|repo| {
            let object_id = tree.oid.to_gix()?;
            let tree_object = repo
                .find_tree(object_id)
                .map_err(|source| StoreError::ReadTree {
                    oid: tree.oid.clone(),
                    source: anyhow::Error::new(source),
                })?;
            let decoded = tree_object
                .decode()
                .map_err(|source| StoreError::DecodeTree {
                    oid: tree.oid.clone(),
                    source,
                })?;

            Ok(decoded
                .entries
                .into_iter()
                .map(|entry| TreeEntry {
                    name: entry.filename.iter().copied().collect(),
                    oid: GitObjectId::from_gix(entry.oid),
                    mode: u32::from(*entry.mode),
                    kind: GitObjectKind::from_entry_kind(entry.mode.kind()),
                })
                .collect::<Vec<_>>())
        })?;

        let mut cache = match self.caches.trees.lock() {
            Ok(cache) => cache,
            Err(poisoned) => poisoned.into_inner(),
        };
        let _ = cache.insert(tree.oid.clone(), entries.clone());
        Ok(entries)
    }

    fn cached_small_blob(&self, oid: &GitObjectId) -> Option<Arc<[u8]>> {
        let mut cache = match self.caches.small_blobs.lock() {
            Ok(cache) => cache,
            Err(poisoned) => poisoned.into_inner(),
        };
        cache.get_cloned(oid)
    }

    fn cache_small_blob(&self, oid: &GitObjectId, bytes: Arc<[u8]>) {
        let mut cache = match self.caches.small_blobs.lock() {
            Ok(cache) => cache,
            Err(poisoned) => poisoned.into_inner(),
        };
        let _ = cache.insert(oid.clone(), bytes);
    }

    fn read_blob_len(&self, oid: &GitObjectId) -> Result<u64, StoreError> {
        self.with_thread_local_repo(|repo| {
            let object_id = oid.to_gix()?;
            repo.find_header(object_id)
                .map(|header| header.size())
                .map_err(|source| StoreError::ReadBlob {
                    oid: oid.clone(),
                    source: anyhow::Error::new(source),
                })
        })
    }

    fn read_blob_bytes_uncached(&self, oid: &GitObjectId) -> Result<Vec<u8>, StoreError> {
        self.metrics
            .blob_backend_reads
            .fetch_add(1, Ordering::Relaxed);
        self.with_thread_local_repo(|repo| {
            let object_id = oid.to_gix()?;
            let mut blob = repo
                .find_blob(object_id)
                .map_err(|source| StoreError::ReadBlob {
                    oid: oid.clone(),
                    source: anyhow::Error::new(source),
                })?;
            Ok(blob.take_data())
        })
    }

    fn read_cached_blob_bytes(&self, oid: &GitObjectId) -> Result<Arc<[u8]>, StoreError> {
        if let Some(bytes) = self.cached_small_blob(oid) {
            self.metrics.blob_cache_hits.fetch_add(1, Ordering::Relaxed);
            return Ok(bytes);
        }

        self.metrics
            .blob_cache_misses
            .fetch_add(1, Ordering::Relaxed);

        let bytes: Arc<[u8]> = Arc::from(self.read_blob_bytes_uncached(oid)?);
        if bytes.len() <= self.cache_layout.small_blob_threshold {
            self.cache_small_blob(oid, Arc::clone(&bytes));
        }
        Ok(bytes)
    }

    fn open_large_blob(&self, oid: &GitObjectId) -> Result<BlobReader, StoreError> {
        self.metrics
            .blob_streamed_opens
            .fetch_add(1, Ordering::Relaxed);

        let unique = self.temp_blob_counter.fetch_add(1, Ordering::Relaxed);
        let temp_path = std::env::temp_dir().join(format!(
            "timefs-blob-{}-{}-{unique}",
            std::process::id(),
            oid.as_hex()
        ));
        let mut file = OpenOptions::new()
            .write(true)
            .read(true)
            .create_new(true)
            .open(&temp_path)
            .map_err(|source| StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::Error::new(source),
            })?;

        let mut child = Command::new("git")
            .arg("-C")
            .arg(&self.repo_path)
            .args(["cat-file", "blob", oid.as_hex()])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .map_err(|source| StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::Error::new(source),
            })?;

        let copy_result = (|| -> Result<(), StoreError> {
            let mut stdout = child.stdout.take().ok_or_else(|| StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::anyhow!("git cat-file did not provide a stdout pipe"),
            })?;
            std::io::copy(&mut stdout, &mut file).map_err(|source| StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::Error::new(source),
            })?;
            file.flush().map_err(|source| StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::Error::new(source),
            })?;
            Ok(())
        })();

        let mut stderr = Vec::new();
        if let Some(mut pipe) = child.stderr.take() {
            pipe.read_to_end(&mut stderr)
                .map_err(|source| StoreError::ReadBlob {
                    oid: oid.clone(),
                    source: anyhow::Error::new(source),
                })?;
        }
        let status = child.wait().map_err(|source| StoreError::ReadBlob {
            oid: oid.clone(),
            source: anyhow::Error::new(source),
        })?;

        if let Err(error) = copy_result {
            let _ = fs::remove_file(&temp_path);
            return Err(error);
        }

        if !status.success() {
            let _ = fs::remove_file(&temp_path);
            return Err(StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::anyhow!(
                    "git cat-file blob exited with {status}: {}",
                    String::from_utf8_lossy(&stderr).trim()
                ),
            });
        }

        file.seek(SeekFrom::Start(0))
            .map_err(|source| StoreError::ReadBlob {
                oid: oid.clone(),
                source: anyhow::Error::new(source),
            })?;
        Ok(Box::new(TempBlobFile {
            file,
            path: temp_path,
        }))
    }

    fn lfs_pointer(&self, oid: &GitObjectId) -> Result<Option<LfsPointer>, StoreError> {
        let len = self.read_blob_len(oid)?;
        if len > MAX_LFS_POINTER_BYTES {
            return Ok(None);
        }

        let bytes = self.read_cached_blob_bytes(oid)?;
        Ok(parse_lfs_pointer(bytes.as_ref()))
    }

    fn locate_resolved_lfs_object(
        &self,
        oid: &GitObjectId,
    ) -> Result<Option<ResolvedLfsObject>, StoreError> {
        if !self.lfs_enabled {
            return Ok(None);
        }

        let Some(pointer) = self.lfs_pointer(oid)? else {
            return Ok(None);
        };
        let path = self
            .git_dir
            .join("lfs")
            .join("objects")
            .join(&pointer.oid[..2])
            .join(&pointer.oid[2..4])
            .join(&pointer.oid);
        let metadata = match fs::metadata(&path) {
            Ok(metadata) => metadata,
            Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(source) => {
                return Err(StoreError::ReadBlob {
                    oid: oid.clone(),
                    source: anyhow::Error::new(source),
                });
            }
        };

        Ok(Some(ResolvedLfsObject {
            path,
            size: metadata.len(),
        }))
    }

    fn open_resolved_lfs_object(
        &self,
        oid: &GitObjectId,
        resolved: ResolvedLfsObject,
    ) -> Result<BlobReader, StoreError> {
        let file = File::open(&resolved.path).map_err(|source| StoreError::ReadBlob {
            oid: oid.clone(),
            source: anyhow::Error::new(source),
        })?;
        Ok(Box::new(file))
    }

    fn cached_revision(&self, key: &RevisionCacheKey) -> Option<CommitInfo> {
        let mut cache = match self.caches.revisions.lock() {
            Ok(cache) => cache,
            Err(poisoned) => poisoned.into_inner(),
        };
        cache.get_cloned(key)
    }

    fn cache_revision(&self, key: RevisionCacheKey, commit: CommitInfo) {
        let mut cache = match self.caches.revisions.lock() {
            Ok(cache) => cache,
            Err(poisoned) => poisoned.into_inner(),
        };
        cache.insert(key, commit);
    }

    fn capture_reference_snapshot(&self) -> Result<ReferenceSnapshot, StoreError> {
        let head = self.resolve_head_live()?;
        let references = self.list_references_live()?;
        let mut revisions = Vec::new();
        revisions.push(SnapshotRevision {
            name: b"HEAD".to_vec(),
            commit: head.clone(),
        });

        for reference in &references {
            let commit = self.resolve_revision_live(&reference.full_name)?;
            revisions.push(SnapshotRevision {
                name: reference.full_name.clone(),
                commit: commit.clone(),
            });
            if let Some(logical_name) = logical_reference_name(&reference.full_name) {
                revisions.push(SnapshotRevision {
                    name: logical_name.to_vec(),
                    commit,
                });
            }
        }

        revisions.sort_by(|left, right| {
            right
                .name
                .len()
                .cmp(&left.name.len())
                .then_with(|| left.name.cmp(&right.name))
        });
        revisions.dedup_by(|left, right| left.name == right.name);

        Ok(ReferenceSnapshot {
            head,
            references,
            revisions,
        })
    }

    fn resolve_head_live(&self) -> Result<CommitInfo, StoreError> {
        self.metrics
            .revision_backend_resolves
            .fetch_add(1, Ordering::Relaxed);
        self.with_thread_local_repo(|repo| {
            let commit = repo
                .head_commit()
                .map_err(|source| StoreError::ResolveHead {
                    source: anyhow::Error::new(source),
                })?;
            let time = commit.time().map_err(|source| StoreError::ReadCommit {
                oid: GitObjectId::from_gix(commit.id.as_ref()),
                source: anyhow::Error::new(source),
            })?;

            Ok(CommitInfo {
                oid: GitObjectId::from_gix(commit.id.as_ref()),
                committer_time_seconds: time.seconds,
            })
        })
    }

    fn resolve_head_uncached(&self) -> Result<CommitInfo, StoreError> {
        if let Some(snapshot) = &self.reference_snapshot {
            return Ok(snapshot.head.clone());
        }
        self.resolve_head_live()
    }

    fn resolve_revision_live(&self, spec: &[u8]) -> Result<CommitInfo, StoreError> {
        self.metrics
            .revision_backend_resolves
            .fetch_add(1, Ordering::Relaxed);
        self.with_thread_local_repo(|repo| {
            let id = repo
                .rev_parse_single(spec.as_bstr())
                .map_err(|source| map_revision_error(repo, spec, source))?;
            self.commit_info_from_id(spec, id)
        })
    }

    fn resolve_revision_uncached(&self, spec: &[u8]) -> Result<CommitInfo, StoreError> {
        let Some(snapshot) = &self.reference_snapshot else {
            return self.resolve_revision_live(spec);
        };

        let (base, suffix) = split_revision_base(spec);
        let commit = if let Some(commit) = snapshot.resolve_named_commit(base) {
            commit
        } else if looks_like_object_prefix(base) {
            self.resolve_commit_prefix_live(base)?
        } else {
            return Err(StoreError::RevisionNotFound {
                spec: display_spec(spec),
                source: anyhow::anyhow!("not present in ref snapshot"),
            });
        };

        if suffix.is_empty() {
            return Ok(commit);
        }

        let mut rewritten = commit.oid.as_hex().as_bytes().to_vec();
        rewritten.extend_from_slice(suffix);
        self.resolve_revision_live(&rewritten)
    }

    fn resolve_commit_prefix_live(&self, prefix: &[u8]) -> Result<CommitInfo, StoreError> {
        self.metrics
            .revision_backend_resolves
            .fetch_add(1, Ordering::Relaxed);
        self.with_thread_local_repo(|repo| {
            let parsed = gix::revision::Spec::from_bstr(
                prefix.as_bstr(),
                repo,
                gix::revision::spec::parse::Options {
                    refs_hint: gix::revision::spec::parse::RefsHint::PreferObject,
                    object_kind_hint: Some(gix::revision::spec::parse::ObjectKindHint::Commit),
                },
            )
            .map_err(|source| map_revision_error(repo, prefix, source))?;
            let id = parsed
                .single()
                .ok_or(gix::revision::spec::parse::single::Error::RangedRev {
                    spec: prefix.as_bstr().to_owned(),
                })
                .map_err(|source| map_revision_error(repo, prefix, source))?;
            self.commit_info_from_id(prefix, id)
        })
    }

    fn resolve_commit_prefix_uncached(&self, prefix: &[u8]) -> Result<CommitInfo, StoreError> {
        self.resolve_commit_prefix_live(prefix)
    }

    fn commit_info_from_id(&self, spec: &[u8], id: gix::Id<'_>) -> Result<CommitInfo, StoreError> {
        let object = id.object().map_err(|source| StoreError::ReadCommit {
            oid: GitObjectId::from_gix(id.as_ref()),
            source: anyhow::Error::new(source),
        })?;
        let commit = object
            .peel_to_kind(gix::object::Kind::Commit)
            .map_err(|source| StoreError::ResolveRevision {
                spec: display_spec(spec),
                source: anyhow::Error::new(source),
            })?
            .into_commit();
        let time = commit.time().map_err(|source| StoreError::ReadCommit {
            oid: GitObjectId::from_gix(commit.id.as_ref()),
            source: anyhow::Error::new(source),
        })?;

        Ok(CommitInfo {
            oid: GitObjectId::from_gix(commit.id.as_ref()),
            committer_time_seconds: time.seconds,
        })
    }

    fn read_commit_info(&self, oid: &GitObjectId) -> Result<CommitInfo, StoreError> {
        self.with_thread_local_repo(|repo| {
            let object_id = oid.to_gix()?;
            let commit = repo
                .find_commit(object_id)
                .map_err(|source| StoreError::ReadCommit {
                    oid: oid.clone(),
                    source: anyhow::Error::new(source),
                })?;
            let time = commit.time().map_err(|source| StoreError::ReadCommit {
                oid: oid.clone(),
                source: anyhow::Error::new(source),
            })?;

            Ok(CommitInfo {
                oid: oid.clone(),
                committer_time_seconds: time.seconds,
            })
        })
    }

    fn run_git_bytes<I, S>(&self, args: I, history_path: &[u8]) -> Result<Vec<u8>, StoreError>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let output = Command::new("git")
            .arg("-C")
            .arg(&self.repo_path)
            .args(args)
            .arg("--")
            .arg(OsString::from_vec(history_path.to_vec()))
            .output()
            .map_err(|source| StoreError::HistoryLookup {
                path: display_spec(history_path),
                source: anyhow::Error::new(source),
            })?;

        if output.status.success() {
            Ok(output.stdout)
        } else {
            Err(StoreError::HistoryLookup {
                path: display_spec(history_path),
                source: anyhow::anyhow!(
                    "git exited with {}: {}",
                    output.status,
                    String::from_utf8_lossy(&output.stderr).trim()
                ),
            })
        }
    }

    fn read_history_entries(&self, path: &[u8]) -> Result<Vec<HistoryEntry>, StoreError> {
        let mut args = vec![
            OsString::from("log"),
            OsString::from("--follow"),
            OsString::from("-z"),
            OsString::from("--format=commit:%H"),
            OsString::from("--name-status"),
            OsString::from("--diff-filter=AMCRT"),
        ];
        if let Some(snapshot) = &self.reference_snapshot {
            args.push(OsString::from(snapshot.head.oid.as_hex()));
        }
        let stdout = self.run_git_bytes(args, path)?;

        let mut tokens = stdout
            .split(|byte| *byte == 0)
            .filter(|token| !token.is_empty());
        let mut entries = Vec::new();
        let mut saw_exact_match = false;

        while let Some(commit_token) = tokens.next() {
            let Some(hex) = commit_token.strip_prefix(b"commit:") else {
                continue;
            };
            let commit_hex =
                String::from_utf8(hex.to_vec()).map_err(|source| StoreError::HistoryLookup {
                    path: display_spec(path),
                    source: anyhow::Error::new(source),
                })?;
            let commit_oid = GitObjectId(commit_hex);
            let commit = self.read_commit_info(&commit_oid)?;

            let Some(status_token) = tokens.next() else {
                break;
            };
            let status = status_token.strip_prefix(b"\n").unwrap_or(status_token);
            let Some(kind) = status.first().copied() else {
                continue;
            };

            let Some(first_path) = tokens.next() else {
                break;
            };
            let path_at_commit = match kind {
                b'R' | b'C' => {
                    let Some(second_path) = tokens.next() else {
                        break;
                    };
                    second_path.to_vec()
                }
                _ => first_path.to_vec(),
            };

            if path_at_commit == path {
                saw_exact_match = true;
            }

            entries.push(HistoryEntry {
                commit,
                path: path_at_commit,
            });
        }

        entries.reverse();
        if saw_exact_match {
            Ok(entries)
        } else {
            Ok(Vec::new())
        }
    }

    fn read_history_prefix_exists(&self, prefix: &[u8]) -> Result<bool, StoreError> {
        let mut args = vec![
            OsString::from("log"),
            OsString::from("-z"),
            OsString::from("--format="),
            OsString::from("--name-only"),
        ];
        if let Some(snapshot) = &self.reference_snapshot {
            args.push(OsString::from(snapshot.head.oid.as_hex()));
        }
        let stdout = self.run_git_bytes(args, prefix)?;
        let mut slash_prefix = prefix.to_vec();
        slash_prefix.push(b'/');
        Ok(stdout
            .split(|byte| *byte == 0)
            .map(|token| token.strip_prefix(b"\n").unwrap_or(token))
            .any(|token| token.starts_with(&slash_prefix)))
    }
}

impl ObjectStore for GixRepository {
    fn open(path: &Path) -> Result<Self, StoreError> {
        Self::open_with_cache_size_mb(path, DEFAULT_CACHE_SIZE_MB)
    }

    fn root_tree(&self, commit: &CommitInfo) -> Result<TreeInfo, StoreError> {
        self.with_thread_local_repo(|repo| {
            let object_id = commit.oid.to_gix()?;
            let commit_object =
                repo.find_commit(object_id)
                    .map_err(|source| StoreError::ReadCommit {
                        oid: commit.oid.clone(),
                        source: anyhow::Error::new(source),
                    })?;
            let tree = commit_object
                .tree()
                .map_err(|source| StoreError::ReadCommit {
                    oid: commit.oid.clone(),
                    source: anyhow::Error::new(source),
                })?;

            Ok(TreeInfo {
                oid: GitObjectId::from_gix(tree.id.as_ref()),
            })
        })
    }

    fn list_tree(&self, tree: &TreeInfo) -> Result<Vec<TreeEntry>, StoreError> {
        self.cached_tree_entries(tree)
    }

    fn blob_len(&self, oid: &GitObjectId) -> Result<u64, StoreError> {
        if let Some(resolved) = self.locate_resolved_lfs_object(oid)? {
            return Ok(resolved.size);
        }
        self.read_blob_len(oid)
    }

    fn open_blob(&self, oid: &GitObjectId) -> Result<BlobReader, StoreError> {
        if let Some(resolved) = self.locate_resolved_lfs_object(oid)? {
            return self.open_resolved_lfs_object(oid, resolved);
        }

        let len = self.read_blob_len(oid)?;
        if usize::try_from(len)
            .ok()
            .is_some_and(|size| size <= self.cache_layout.small_blob_threshold)
        {
            let bytes = self.read_cached_blob_bytes(oid)?;
            return Ok(Box::new(Cursor::new(bytes)));
        }

        self.open_large_blob(oid)
    }

    fn read_symlink_target(&self, oid: &GitObjectId) -> Result<Vec<u8>, StoreError> {
        Ok(self.read_cached_blob_bytes(oid)?.as_ref().to_vec())
    }

    fn lfs_state(&self, oid: &GitObjectId) -> Result<Option<LfsState>, StoreError> {
        let Some(_) = self.lfs_pointer(oid)? else {
            return Ok(None);
        };

        if self.locate_resolved_lfs_object(oid)?.is_some() {
            Ok(Some(LfsState::Resolved))
        } else {
            Ok(Some(LfsState::Pointer))
        }
    }

    fn open_submodule(
        &self,
        path: &[u8],
        commit: &GitObjectId,
    ) -> Result<Option<OpenedSubmodule<Self>>, StoreError> {
        let submodule_worktree = self.repo_path.join(OsString::from_vec(path.to_vec()));
        let git_marker = submodule_worktree.join(".git");
        let locatable = match git_marker.try_exists() {
            Ok(locatable) => locatable,
            Err(source) => {
                return Err(StoreError::OpenRepository {
                    path: submodule_worktree.clone(),
                    source: anyhow::Error::new(source),
                });
            }
        };
        if !locatable {
            return Ok(None);
        }

        let submodule_repo =
            gix::ThreadSafeRepository::discover(&submodule_worktree).map_err(|source| {
                StoreError::OpenRepository {
                    path: submodule_worktree.clone(),
                    source: anyhow::Error::new(source),
                }
            })?;

        let store = Self::new(submodule_repo, self.cache_layout, self.lfs_enabled, false)?;
        let pinned_commit = store.read_commit_info(commit)?;
        let tree = store.root_tree(&pinned_commit)?;
        Ok(Some(OpenedSubmodule {
            store,
            commit: pinned_commit,
            tree,
        }))
    }

    fn history_entries(&self, path: &[u8]) -> Result<Vec<HistoryEntry>, StoreError> {
        self.read_history_entries(path)
    }

    fn history_prefix_exists(&self, prefix: &[u8]) -> Result<bool, StoreError> {
        self.read_history_prefix_exists(prefix)
    }
}

impl RevResolver for GixRepository {
    fn resolve_head(&self) -> Result<CommitInfo, StoreError> {
        let key = RevisionCacheKey::Head;
        if let Some(commit) = self.cached_revision(&key) {
            self.metrics
                .revision_cache_hits
                .fetch_add(1, Ordering::Relaxed);
            return Ok(commit);
        }

        self.metrics
            .revision_cache_misses
            .fetch_add(1, Ordering::Relaxed);
        let commit = self.resolve_head_uncached()?;
        self.cache_revision(key, commit.clone());
        Ok(commit)
    }

    fn resolve_revision(&self, spec: &[u8]) -> Result<CommitInfo, StoreError> {
        let key = RevisionCacheKey::Revision(spec.to_vec());
        if let Some(commit) = self.cached_revision(&key) {
            self.metrics
                .revision_cache_hits
                .fetch_add(1, Ordering::Relaxed);
            return Ok(commit);
        }

        self.metrics
            .revision_cache_misses
            .fetch_add(1, Ordering::Relaxed);
        let commit = self.resolve_revision_uncached(spec)?;
        self.cache_revision(key, commit.clone());
        Ok(commit)
    }

    fn resolve_commit_prefix(&self, prefix: &[u8]) -> Result<CommitInfo, StoreError> {
        let key = RevisionCacheKey::CommitPrefix(prefix.to_vec());
        if let Some(commit) = self.cached_revision(&key) {
            self.metrics
                .revision_cache_hits
                .fetch_add(1, Ordering::Relaxed);
            return Ok(commit);
        }

        self.metrics
            .revision_cache_misses
            .fetch_add(1, Ordering::Relaxed);
        let commit = self.resolve_commit_prefix_uncached(prefix)?;
        self.cache_revision(key, commit.clone());
        Ok(commit)
    }

    fn list_references(&self) -> Result<Vec<GitReference>, StoreError> {
        if let Some(snapshot) = &self.reference_snapshot {
            return Ok(snapshot.references.clone());
        }

        self.list_references_live()
    }
}

impl GixRepository {
    fn list_references_live(&self) -> Result<Vec<GitReference>, StoreError> {
        self.with_thread_local_repo(|repo| {
            let platform = repo
                .references()
                .map_err(|source| StoreError::ListReferences {
                    source: anyhow::Error::new(source),
                })?;
            let iter = platform
                .all()
                .map_err(|source| StoreError::ListReferences {
                    source: anyhow::Error::new(source),
                })?;

            let mut references = Vec::new();
            for reference in iter {
                let reference = reference.map_err(|source| StoreError::ListReferences {
                    source: anyhow::Error::msg(source.to_string()),
                })?;
                let name = reference.name().as_bstr().to_vec();
                if name.starts_with(b"refs/heads/")
                    || name.starts_with(b"refs/tags/")
                    || name.starts_with(b"refs/remotes/")
                {
                    references.push(GitReference { full_name: name });
                }
            }

            references.sort_by(|left, right| left.full_name.cmp(&right.full_name));
            Ok(references)
        })
    }
}

fn display_spec(spec: &[u8]) -> String {
    String::from_utf8_lossy(spec).into_owned()
}

fn logical_reference_name(full_name: &[u8]) -> Option<&[u8]> {
    full_name
        .strip_prefix(b"refs/heads/")
        .or_else(|| full_name.strip_prefix(b"refs/tags/"))
        .or_else(|| full_name.strip_prefix(b"refs/remotes/"))
}

fn split_revision_base(spec: &[u8]) -> (&[u8], &[u8]) {
    match spec.iter().position(|byte| *byte == b'^' || *byte == b'~') {
        Some(index) => spec.split_at(index),
        None => (spec, &[]),
    }
}

fn looks_like_object_prefix(spec: &[u8]) -> bool {
    spec.len() >= 4 && spec.iter().all(u8::is_ascii_hexdigit)
}

fn parse_lfs_pointer(bytes: &[u8]) -> Option<LfsPointer> {
    let text = std::str::from_utf8(bytes).ok()?;
    let mut saw_version = false;
    let mut oid = None;

    for line in text.lines() {
        if line == "version https://git-lfs.github.com/spec/v1" {
            saw_version = true;
            continue;
        }

        if let Some(value) = line.strip_prefix("oid sha256:") {
            if value.len() == 64 && value.as_bytes().iter().all(u8::is_ascii_hexdigit) {
                oid = Some(value.to_ascii_lowercase());
            } else {
                return None;
            }
        }
    }

    saw_version.then_some(LfsPointer { oid: oid? })
}

fn map_revision_error(
    repo: &gix::Repository,
    spec: &[u8],
    error: impl Into<gix::revision::spec::parse::single::Error>,
) -> StoreError {
    use gix::revision::spec::parse::single::Error as SingleError;
    use gix::revision::spec::parse::Error as ParseError;
    let error = error.into();
    let spec_text = display_spec(spec);

    match error {
        SingleError::RangedRev { spec } => StoreError::ResolveRevision {
            spec: spec.to_string(),
            source: anyhow::anyhow!("revision resolved to multiple objects"),
        },
        SingleError::Parse(parse_error) => match parse_error {
            ParseError::PrefixNotFound { .. }
            | ParseError::PathNotFound { .. }
            | ParseError::SingleNotFound => StoreError::RevisionNotFound {
                spec: spec_text,
                source: anyhow::Error::new(parse_error),
            },
            ParseError::AncestorOutOfRange { .. } | ParseError::ParentOutOfRange { .. } => {
                if repo.is_shallow() {
                    StoreError::RevisionNotFound {
                        spec: spec_text,
                        source: anyhow::anyhow!("repository is shallow"),
                    }
                } else {
                    StoreError::RevisionNotFound {
                        spec: spec_text,
                        source: anyhow::Error::new(parse_error),
                    }
                }
            }
            ParseError::AmbiguousPrefix { .. } | ParseError::AmbiguousRefAndObject { .. } => {
                StoreError::AmbiguousRevision {
                    spec: spec_text,
                    source: anyhow::Error::new(parse_error),
                }
            }
            ParseError::FindReference(_) => StoreError::RevisionNotFound {
                spec: spec_text,
                source: anyhow::Error::new(parse_error),
            },
            other => StoreError::ResolveRevision {
                spec: spec_text,
                source: anyhow::Error::new(other),
            },
        },
    }
}

#[cfg(test)]
mod tests {
    use super::{
        GitObjectId, GitObjectKind, GitReference, GixRepository, ObjectStore, RevResolver,
        TreeEntry, DEFAULT_SMALL_BLOB_MAX_BYTES,
    };
    use std::ffi::{OsStr, OsString};
    use std::fs;
    use std::io::Read;
    use std::os::unix::ffi::OsStringExt;
    use std::os::unix::fs::{symlink, PermissionsExt};
    use std::path::{Path, PathBuf};
    use std::process::{Command, Output};
    use std::sync::{Arc, Barrier};
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn root_tree_listing_matches_git_ls_tree_head() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open(fixture.path()).expect("fixture repo should open");

        let head = store.resolve_head().expect("HEAD should resolve");
        let root = store.root_tree(&head).expect("root tree should resolve");
        let actual = store.list_tree(&root).expect("tree listing should succeed");

        assert_eq!(actual, fixture.git_ls_tree("HEAD"));
    }

    #[test]
    fn blob_read_matches_git_show_byte_for_byte() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open(fixture.path()).expect("fixture repo should open");
        let blob_oid = fixture.git_rev_parse("HEAD:dir/data.bin");

        let mut reader = store.open_blob(&blob_oid).expect("blob should open");
        let mut actual = Vec::new();
        reader
            .read_to_end(&mut actual)
            .expect("blob reader should read to completion");

        assert_eq!(actual, fixture.git_show("HEAD:dir/data.bin"));
    }

    #[test]
    fn symlink_target_matches_git_blob_content() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open(fixture.path()).expect("fixture repo should open");
        let link_oid = fixture.git_rev_parse("HEAD:link-to-script");

        let actual = store
            .read_symlink_target(&link_oid)
            .expect("symlink target should be readable");

        assert_eq!(actual, b"run.sh".to_vec());
    }

    #[test]
    fn concurrent_reads_succeed_with_per_thread_handles() {
        let fixture = FixtureRepository::new();
        let store =
            Arc::new(GixRepository::open(fixture.path()).expect("fixture repo should open"));
        let blob_oid = fixture.git_rev_parse("HEAD:dir/data.bin");
        let expected = fixture.git_show("HEAD:dir/data.bin");
        let barrier = Arc::new(Barrier::new(5));
        let before = store
            .handle_creations
            .load(std::sync::atomic::Ordering::Relaxed);

        let threads: Vec<_> = (0..4)
            .map(|_| {
                let store = Arc::clone(&store);
                let blob_oid = blob_oid.clone();
                let barrier = Arc::clone(&barrier);
                std::thread::spawn(move || {
                    barrier.wait();

                    let head = store.resolve_head()?;
                    let root = store.root_tree(&head)?;
                    let _ = store.list_tree(&root)?;

                    let mut reader = store.open_blob(&blob_oid)?;
                    let mut actual = Vec::new();
                    reader
                        .read_to_end(&mut actual)
                        .map_err(anyhow::Error::new)?;
                    Ok::<Vec<u8>, anyhow::Error>(actual)
                })
            })
            .collect();

        barrier.wait();

        for thread in threads {
            let actual = thread
                .join()
                .expect("reader thread should not panic")
                .expect("reader thread should succeed");
            assert_eq!(actual, expected);
        }

        let created = store
            .handle_creations
            .load(std::sync::atomic::Ordering::Relaxed)
            - before;
        assert!(
            created >= 4,
            "expected at least one thread-local handle per worker thread, observed {created}"
        );
    }

    #[test]
    fn revision_resolution_matches_git_rev_parse() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open(fixture.path()).expect("fixture repo should open");

        let tag = store.resolve_revision(b"v1.0").expect("tag should resolve");
        let branch = store
            .resolve_revision(b"topic")
            .expect("branch should resolve");
        let parent = store
            .resolve_revision(b"HEAD~1")
            .expect("HEAD~1 should resolve");

        assert_eq!(tag.oid, fixture.git_rev_parse_commit("v1.0"));
        assert_eq!(branch.oid, fixture.git_rev_parse_commit("topic"));
        assert_eq!(parent.oid, fixture.git_rev_parse_commit("HEAD~1"));
    }

    #[test]
    fn abbreviated_commit_resolution_matches_git_rev_parse() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open(fixture.path()).expect("fixture repo should open");
        let short = fixture.git_rev_parse_short("HEAD~1");

        let commit = store
            .resolve_commit_prefix(short.as_bytes())
            .expect("short hash should resolve");

        assert_eq!(commit.oid, fixture.git_rev_parse_commit("HEAD~1"));
    }

    #[test]
    fn reference_enumeration_matches_git_for_each_ref() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open(fixture.path()).expect("fixture repo should open");

        let actual = store
            .list_references()
            .expect("reference enumeration should succeed");

        assert_eq!(
            actual,
            fixture
                .git_for_each_ref()
                .into_iter()
                .map(|full_name| GitReference { full_name })
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn parsed_tree_listings_hit_the_tree_cache_after_the_first_read() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open_with_cache_size_mb(fixture.path(), 1)
            .expect("fixture repo should open");
        let head = store.resolve_head().expect("HEAD should resolve");
        let root = store.root_tree(&head).expect("root tree should resolve");

        let before = snapshot_metrics(&store);
        let first = store
            .list_tree(&root)
            .expect("first tree listing should succeed");
        let after_first = snapshot_metrics(&store);
        let second = store
            .list_tree(&root)
            .expect("second tree listing should succeed");
        let after_second = snapshot_metrics(&store);

        assert_eq!(first, second);
        assert_eq!(
            after_first.tree_backend_reads - before.tree_backend_reads,
            1
        );
        assert_eq!(
            after_second.tree_backend_reads,
            after_first.tree_backend_reads
        );
        assert!(after_second.tree_cache_hits > after_first.tree_cache_hits);
    }

    #[test]
    fn small_blob_reads_reuse_the_blob_cache_while_large_blobs_stream() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open_with_cache_size_mb(fixture.path(), 1)
            .expect("fixture repo should open");
        let small_blob = fixture.git_rev_parse("HEAD:README.md");
        let large_blob = fixture.git_rev_parse("HEAD:large.bin");

        let first_small = read_all(
            store
                .open_blob(&small_blob)
                .expect("small blob should open"),
        );
        assert_eq!(first_small, fixture.git_show("HEAD:README.md"));
        let after_small_first = snapshot_metrics(&store);
        let second_small = read_all(
            store
                .open_blob(&small_blob)
                .expect("small blob should reopen"),
        );
        assert_eq!(second_small, fixture.git_show("HEAD:README.md"));
        let after_small_second = snapshot_metrics(&store);
        assert_eq!(
            after_small_second.blob_backend_reads,
            after_small_first.blob_backend_reads
        );
        assert!(after_small_second.blob_cache_hits > after_small_first.blob_cache_hits);

        let first_large = read_all(
            store
                .open_blob(&large_blob)
                .expect("large blob should open"),
        );
        assert_eq!(first_large, fixture.git_show("HEAD:large.bin"));
        let after_large_first = snapshot_metrics(&store);
        let second_large = read_all(
            store
                .open_blob(&large_blob)
                .expect("large blob should reopen"),
        );
        assert_eq!(second_large, fixture.git_show("HEAD:large.bin"));
        let after_large_second = snapshot_metrics(&store);
        assert_eq!(
            after_large_second.blob_streamed_opens - after_large_first.blob_streamed_opens,
            1
        );
    }

    #[test]
    fn revision_resolution_hits_the_short_ttl_cache() {
        let fixture = FixtureRepository::new();
        let store = GixRepository::open_with_cache_size_mb(fixture.path(), 1)
            .expect("fixture repo should open");

        let before = snapshot_metrics(&store);
        let first = store
            .resolve_revision(b"topic")
            .expect("first revision resolution should succeed");
        let after_first = snapshot_metrics(&store);
        let second = store
            .resolve_revision(b"topic")
            .expect("second revision resolution should succeed");
        let after_second = snapshot_metrics(&store);

        assert_eq!(first, second);
        assert_eq!(
            after_first.revision_backend_resolves - before.revision_backend_resolves,
            1
        );
        assert_eq!(
            after_second.revision_backend_resolves,
            after_first.revision_backend_resolves
        );
        assert!(after_second.revision_cache_hits > after_first.revision_cache_hits);
    }

    #[test]
    fn object_caches_stay_within_the_configured_budget() {
        let fixture = BulkFixtureRepository::new();
        let store = GixRepository::open_with_cache_size_mb(fixture.path(), 1)
            .expect("fixture repo should open");
        let head = store.resolve_head().expect("HEAD should resolve");
        let root = store.root_tree(&head).expect("root tree should resolve");

        for entry in store
            .list_tree(&root)
            .expect("root tree listing should succeed")
        {
            if entry.kind == GitObjectKind::Blob {
                let _ = read_all(store.open_blob(&entry.oid).expect("bulk blob should open"));
            }
        }

        let metrics = snapshot_metrics(&store);
        assert!(metrics.tree_cache_weight <= store.cache_layout.tree_cache_bytes);
        assert!(metrics.blob_cache_weight <= store.cache_layout.blob_cache_bytes);
    }

    struct FixtureRepository {
        root: PathBuf,
    }

    impl FixtureRepository {
        fn new() -> Self {
            let root = unique_temp_path("timefs-store-fixture");
            fs::create_dir_all(root.join("dir"))
                .expect("fixture directory creation should succeed");

            let fixture = Self { root };
            fixture.git(["init", "-b", "main"]);
            fixture.git(["config", "user.name", "Timefs Tests"]);
            fixture.git(["config", "user.email", "timefs-tests@example.com"]);

            fs::write(fixture.root.join("README.md"), b"timefs fixture\n")
                .expect("README write should succeed");
            fs::write(fixture.root.join("dir/data.bin"), [0_u8, 1, 2, 3, 0, 255])
                .expect("binary write should succeed");
            let large_blob: Vec<u8> = (0..(DEFAULT_SMALL_BLOB_MAX_BYTES * 2))
                .map(|index| u8::try_from(index % 251).expect("modulus keeps the byte in range"))
                .collect();
            fs::write(fixture.root.join("large.bin"), large_blob)
                .expect("large blob write should succeed");
            fs::write(fixture.root.join("run.sh"), b"#!/bin/sh\necho timefs\n")
                .expect("script write should succeed");
            symlink("run.sh", fixture.root.join("link-to-script"))
                .expect("symlink creation should succeed");

            let mut permissions = fs::metadata(fixture.root.join("run.sh"))
                .expect("script metadata should be readable")
                .permissions();
            permissions.set_mode(0o755);
            fs::set_permissions(fixture.root.join("run.sh"), permissions)
                .expect("script permissions should be settable");

            fixture.git(["add", "."]);
            fixture.git(["commit", "-m", "Create fixture repository"]);
            fixture.git(["tag", "v1.0"]);

            fs::write(fixture.root.join("README.md"), b"timefs fixture v2\n")
                .expect("updated README write should succeed");
            fs::write(fixture.root.join("topic.txt"), b"branch and tag fixture\n")
                .expect("topic marker write should succeed");
            fixture.git(["add", "."]);
            fixture.git(["commit", "-m", "Update main branch"]);
            fixture.git(["branch", "topic"]);
            fixture.git(["branch", "feature/x"]);

            fs::write(fixture.root.join("later.txt"), b"current HEAD\n")
                .expect("later marker write should succeed");
            fixture.git(["add", "."]);
            fixture.git(["commit", "-m", "Advance HEAD"]);

            fixture
        }

        fn path(&self) -> &Path {
            &self.root
        }

        fn git<I, S>(&self, args: I) -> Output
        where
            I: IntoIterator<Item = S>,
            S: AsRef<OsStr>,
        {
            let output = Command::new("git")
                .args(args)
                .current_dir(&self.root)
                .output()
                .expect("git should be executable in tests");

            if output.status.success() {
                output
            } else {
                panic!(
                    "git command failed with status {}: {}",
                    output.status,
                    String::from_utf8_lossy(&output.stderr)
                );
            }
        }

        fn git_ls_tree(&self, rev: &str) -> Vec<TreeEntry> {
            parse_ls_tree_entries(&self.git(["ls-tree", "-z", rev]).stdout)
        }

        fn git_rev_parse(&self, spec: &str) -> GitObjectId {
            let output = self.git(["rev-parse", spec]);
            let hex = String::from_utf8(output.stdout)
                .expect("rev-parse output should be valid UTF-8")
                .trim()
                .to_owned();
            GitObjectId(hex)
        }

        fn git_rev_parse_commit(&self, spec: &str) -> GitObjectId {
            self.git_rev_parse(&format!("{spec}^{{commit}}"))
        }

        fn git_rev_parse_short(&self, spec: &str) -> String {
            let output = self.git(["rev-parse", "--short", spec]);
            String::from_utf8(output.stdout)
                .expect("short rev-parse output should be valid UTF-8")
                .trim()
                .to_owned()
        }

        fn git_show(&self, spec: &str) -> Vec<u8> {
            self.git(["show", spec]).stdout
        }

        fn git_for_each_ref(&self) -> Vec<Vec<u8>> {
            self.git([
                "for-each-ref",
                "--format=%(refname)",
                "refs/heads",
                "refs/tags",
                "refs/remotes",
            ])
            .stdout
            .split(|byte| *byte == b'\n')
            .filter(|line| !line.is_empty())
            .map(|line| line.to_vec())
            .collect()
        }
    }

    impl Drop for FixtureRepository {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.root);
        }
    }

    struct BulkFixtureRepository {
        root: PathBuf,
    }

    impl BulkFixtureRepository {
        fn new() -> Self {
            let root = unique_temp_path("timefs-store-bulk-fixture");
            fs::create_dir_all(&root).expect("fixture directory creation should succeed");

            let fixture = Self { root };
            fixture.git(["init", "-b", "main"]);
            fixture.git(["config", "user.name", "Timefs Tests"]);
            fixture.git(["config", "user.email", "timefs-tests@example.com"]);

            for index in 0..128_usize {
                let path = fixture.root.join(format!("blob-{index:03}.bin"));
                let contents: Vec<u8> = (0..16_384_usize)
                    .map(|offset| {
                        u8::try_from((index + offset) % 251)
                            .expect("modulus keeps the byte in range")
                    })
                    .collect();
                fs::write(path, contents).expect("bulk blob write should succeed");
            }

            fixture.git(["add", "."]);
            fixture.git(["commit", "-m", "Create bulk cache fixture"]);
            fixture
        }

        fn path(&self) -> &Path {
            &self.root
        }

        fn git<I, S>(&self, args: I) -> Output
        where
            I: IntoIterator<Item = S>,
            S: AsRef<OsStr>,
        {
            let output = Command::new("git")
                .args(args)
                .current_dir(&self.root)
                .output()
                .expect("git should be executable in tests");

            if output.status.success() {
                output
            } else {
                panic!(
                    "git command failed with status {}: {}",
                    output.status,
                    String::from_utf8_lossy(&output.stderr)
                );
            }
        }
    }

    impl Drop for BulkFixtureRepository {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.root);
        }
    }

    #[derive(Clone, Copy, Debug)]
    struct MetricSnapshot {
        tree_cache_hits: u64,
        tree_backend_reads: u64,
        blob_cache_hits: u64,
        blob_backend_reads: u64,
        blob_streamed_opens: u64,
        revision_cache_hits: u64,
        revision_backend_resolves: u64,
        tree_cache_weight: usize,
        blob_cache_weight: usize,
    }

    fn snapshot_metrics(store: &GixRepository) -> MetricSnapshot {
        let tree_cache_weight = match store.caches.trees.lock() {
            Ok(cache) => cache.current_weight(),
            Err(poisoned) => poisoned.into_inner().current_weight(),
        };
        let blob_cache_weight = match store.caches.small_blobs.lock() {
            Ok(cache) => cache.current_weight(),
            Err(poisoned) => poisoned.into_inner().current_weight(),
        };

        MetricSnapshot {
            tree_cache_hits: store
                .metrics
                .tree_cache_hits
                .load(std::sync::atomic::Ordering::Relaxed),
            tree_backend_reads: store
                .metrics
                .tree_backend_reads
                .load(std::sync::atomic::Ordering::Relaxed),
            blob_cache_hits: store
                .metrics
                .blob_cache_hits
                .load(std::sync::atomic::Ordering::Relaxed),
            blob_backend_reads: store
                .metrics
                .blob_backend_reads
                .load(std::sync::atomic::Ordering::Relaxed),
            blob_streamed_opens: store
                .metrics
                .blob_streamed_opens
                .load(std::sync::atomic::Ordering::Relaxed),
            revision_cache_hits: store
                .metrics
                .revision_cache_hits
                .load(std::sync::atomic::Ordering::Relaxed),
            revision_backend_resolves: store
                .metrics
                .revision_backend_resolves
                .load(std::sync::atomic::Ordering::Relaxed),
            tree_cache_weight,
            blob_cache_weight,
        }
    }

    fn read_all(mut reader: super::BlobReader) -> Vec<u8> {
        let mut actual = Vec::new();
        reader
            .read_to_end(&mut actual)
            .expect("blob reader should read to completion");
        actual
    }

    fn parse_ls_tree_entries(output: &[u8]) -> Vec<TreeEntry> {
        output
            .split(|byte| *byte == 0)
            .filter(|record| !record.is_empty())
            .map(parse_ls_tree_entry)
            .collect()
    }

    fn parse_ls_tree_entry(record: &[u8]) -> TreeEntry {
        let tab = record
            .iter()
            .position(|byte| *byte == b'\t')
            .expect("ls-tree records contain a tab separator");
        let (header, name) = record.split_at(tab);
        let name = name[1..].to_vec();
        let header = std::str::from_utf8(header).expect("ls-tree headers are valid UTF-8");
        let mut parts = header.split_whitespace();
        let mode = u32::from_str_radix(parts.next().expect("mode should be present"), 8)
            .expect("mode should be valid octal");
        let kind = match parts.next().expect("kind should be present") {
            "tree" => GitObjectKind::Tree,
            "blob" => GitObjectKind::Blob,
            "commit" => GitObjectKind::Commit,
            other => panic!("unexpected git ls-tree kind: {other}"),
        };
        let oid = GitObjectId(parts.next().expect("oid should be present").to_owned());

        TreeEntry {
            name,
            oid,
            mode,
            kind,
        }
    }

    fn unique_temp_path(prefix: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after the Unix epoch")
            .as_nanos();
        let mut path = std::env::temp_dir();
        let suffix = format!("{prefix}-{nanos}");
        path.push(OsString::from_vec(suffix.into_bytes()));
        path
    }
}
