//! `brittle-core` — the library crate for the Brittle literature management system.
//!
//! All operations go through the [`Brittle`] struct. The API is transport-agnostic:
//! every return type is plain data that any layer (UI, REST, CLI) can use directly.

pub mod bibtex;
pub mod error;
pub mod model;
pub mod store;

pub use brittle_model::ReferenceSummary;
pub use error::{BibtexError, BrittleError, StoreError, ValidationError};
pub use model::{
    Annotation, AnnotationId, AnnotationSet, AnnotationType, Color, EntryType, Library, LibraryId,
    PdfAttachment, Person, Point, Quad, Rect, Reference, ReferenceId, Snapshot, TextMarkupType,
};
pub use store::{FsStore, MemoryStore, Store};

use crate::bibtex::export_references;
use crate::error::EntityType;
use chrono::Utc;
use std::path::{Path, PathBuf};

/// The main entry point for Brittle.
///
/// Generic over [`Store`] to allow testing with [`MemoryStore`] and production
/// use with [`FsStore`] without duplicating business logic.
pub struct Brittle<S: Store> {
    store: S,
}

// ---- Constructors ----

impl Brittle<FsStore> {
    /// Create a new Brittle repository at the given path.
    pub fn create(path: &Path) -> Result<Self, BrittleError> {
        let store = FsStore::create(path)?;
        Ok(Self { store })
    }

    /// Open an existing Brittle repository.
    pub fn open(path: &Path) -> Result<Self, BrittleError> {
        let store = FsStore::open(path)?;
        Ok(Self { store })
    }

    /// Returns the root path of the open repository.
    pub fn repository_root(&self) -> &Path {
        self.store.root()
    }

    /// Return the absolute filesystem path to the PDF attached to a reference.
    ///
    /// Returns [`ValidationError::NoPdfAttached`] if the reference has no PDF.
    pub fn get_pdf_path(&self, ref_id: ReferenceId) -> Result<PathBuf, BrittleError> {
        let reference = self.store.load_reference(ref_id)?;
        reference
            .pdf
            .map(|att| self.store.root().join(&att.stored_path))
            .ok_or_else(|| {
                ValidationError::NoPdfAttached {
                    reference_id: ref_id.to_string(),
                }
                .into()
            })
    }

    /// Attach a PDF to a reference.
    ///
    /// Copies the file from `source_path` into the repository's `pdfs/` directory,
    /// computes its SHA-256 content hash, and updates the reference record.
    pub fn attach_pdf(
        &mut self,
        id: ReferenceId,
        source_path: &Path,
    ) -> Result<PdfAttachment, BrittleError> {
        use sha2::{Digest, Sha256};

        if !source_path.exists() {
            return Err(ValidationError::PdfNotFound {
                path: source_path.to_owned(),
            }
            .into());
        }

        let mut reference = self.store.load_reference(id)?;
        let dest_name = format!("{id}.pdf");
        let dest_path = self.store.pdf_dir().join(&dest_name);
        let stored_path = Path::new("pdfs").join(&dest_name);

        let bytes = std::fs::read(source_path).map_err(StoreError::Io)?;
        let mut hasher = Sha256::new();
        hasher.update(&bytes);
        let hash = format!("{:x}", hasher.finalize());

        std::fs::write(&dest_path, &bytes).map_err(StoreError::Io)?;

        let attachment = PdfAttachment {
            stored_path,
            content_hash: hash,
        };
        reference.pdf = Some(attachment.clone());
        reference.modified_at = Utc::now();
        self.store.save_reference(&reference)?;

        Ok(attachment)
    }
}

impl<S: Store> Brittle<S> {
    /// Construct with an arbitrary store. Primarily for testing with [`MemoryStore`].
    pub fn with_store(store: S) -> Self {
        Self { store }
    }

    // ---- Search ----

    /// Search all references. Returns summaries whose cite key, title, year, or
    /// author family name contains `query` (case-insensitive). If `query` is
    /// empty, returns all references.
    pub fn search_references(&self, query: &str) -> Result<Vec<ReferenceSummary>, BrittleError> {
        if query.is_empty() {
            return self.list_references();
        }
        let q = query.to_lowercase();
        let ids = self.store.list_reference_ids()?;
        let mut results = Vec::new();
        for id in ids {
            let r = self.store.load_reference(id)?;
            if reference_matches(&r, &q) {
                results.push(ReferenceSummary::from(&r));
            }
        }
        Ok(results)
    }

    /// Search references within a specific library.
    pub fn search_library_references(
        &self,
        library_id: LibraryId,
        query: &str,
    ) -> Result<Vec<ReferenceSummary>, BrittleError> {
        if query.is_empty() {
            return self.list_library_references(library_id);
        }
        let q = query.to_lowercase();
        let library = self.store.load_library(library_id)?;
        let mut results = Vec::new();
        for ref_id in &library.members {
            let r = self.store.load_reference(*ref_id)?;
            if reference_matches(&r, &q) {
                results.push(ReferenceSummary::from(&r));
            }
        }
        Ok(results)
    }

    // ---- Reference CRUD ----

    /// Create a new reference with the given cite key and entry type.
    ///
    /// Returns an error if the cite key is empty or already in use.
    pub fn create_reference(
        &mut self,
        cite_key: impl Into<String>,
        entry_type: EntryType,
    ) -> Result<Reference, BrittleError> {
        let cite_key = cite_key.into();
        if cite_key.is_empty() {
            return Err(ValidationError::EmptyCiteKey.into());
        }
        // Check for duplicate cite keys.
        for id in self.store.list_reference_ids()? {
            let existing = self.store.load_reference(id)?;
            if existing.cite_key == cite_key {
                return Err(ValidationError::DuplicateCiteKey { cite_key }.into());
            }
        }
        let reference = Reference::new(cite_key, entry_type);
        self.store.save_reference(&reference)?;
        Ok(reference)
    }

    /// Get a reference by ID.
    pub fn get_reference(&self, id: ReferenceId) -> Result<Reference, BrittleError> {
        Ok(self.store.load_reference(id)?)
    }

    /// Replace the mutable fields of a reference. `id` and `created_at` are preserved.
    /// `modified_at` is updated automatically.
    pub fn update_reference(
        &mut self,
        mut reference: Reference,
    ) -> Result<Reference, BrittleError> {
        // Ensure the reference exists.
        let existing = self.store.load_reference(reference.id)?;
        reference.created_at = existing.created_at;
        reference.modified_at = Utc::now();

        // Check that the (possibly changed) cite key doesn't conflict.
        if reference.cite_key.is_empty() {
            return Err(ValidationError::EmptyCiteKey.into());
        }
        for id in self.store.list_reference_ids()? {
            if id == reference.id {
                continue;
            }
            let other = self.store.load_reference(id)?;
            if other.cite_key == reference.cite_key {
                return Err(ValidationError::DuplicateCiteKey {
                    cite_key: reference.cite_key,
                }
                .into());
            }
        }

        self.store.save_reference(&reference)?;
        Ok(reference)
    }

    /// Delete a reference and cascade-remove all associated data:
    /// library memberships and annotations.
    pub fn delete_reference(&mut self, id: ReferenceId) -> Result<(), BrittleError> {
        // Ensure it exists first.
        self.store.load_reference(id)?;

        // Remove from all libraries.
        for lib_id in self.store.list_library_ids()? {
            let mut lib = self.store.load_library(lib_id)?;
            if lib.members.remove(&id) {
                lib.modified_at = Utc::now();
                self.store.save_library(&lib)?;
            }
        }

        // Delete annotations.
        self.store.delete_annotations(id)?;

        // Delete the reference itself.
        self.store.delete_reference(id)?;

        Ok(())
    }

    /// List all references as lightweight summaries.
    pub fn list_references(&self) -> Result<Vec<ReferenceSummary>, BrittleError> {
        let ids = self.store.list_reference_ids()?;
        let mut summaries = Vec::with_capacity(ids.len());
        for id in ids {
            let r = self.store.load_reference(id)?;
            summaries.push(ReferenceSummary::from(&r));
        }
        Ok(summaries)
    }

    /// Set a single field on a reference. Use `"title"`, `"year"`, `"journal"`, etc.
    pub fn set_field(
        &mut self,
        id: ReferenceId,
        field: &str,
        value: impl Into<String>,
    ) -> Result<(), BrittleError> {
        let mut reference = self.store.load_reference(id)?;
        reference.fields.insert(field.to_owned(), value.into());
        reference.modified_at = Utc::now();
        self.store.save_reference(&reference)?;
        Ok(())
    }

    /// Remove a field from a reference.
    pub fn remove_field(&mut self, id: ReferenceId, field: &str) -> Result<(), BrittleError> {
        let mut reference = self.store.load_reference(id)?;
        reference.fields.remove(field);
        reference.modified_at = Utc::now();
        self.store.save_reference(&reference)?;
        Ok(())
    }

    // ---- Library CRUD ----

    /// Create a new library.
    pub fn create_library(
        &mut self,
        name: impl Into<String>,
        parent_id: Option<LibraryId>,
    ) -> Result<Library, BrittleError> {
        let name = name.into();
        if name.is_empty() {
            return Err(ValidationError::EmptyLibraryName.into());
        }
        // Verify parent exists if provided.
        if let Some(pid) = parent_id {
            self.store.load_library(pid)?;
        }
        let library = Library::new(name, parent_id);
        self.store.save_library(&library)?;
        Ok(library)
    }

    /// Get a library by ID.
    pub fn get_library(&self, id: LibraryId) -> Result<Library, BrittleError> {
        Ok(self.store.load_library(id)?)
    }

    /// Rename a library.
    pub fn rename_library(
        &mut self,
        id: LibraryId,
        new_name: impl Into<String>,
    ) -> Result<Library, BrittleError> {
        let new_name = new_name.into();
        if new_name.is_empty() {
            return Err(ValidationError::EmptyLibraryName.into());
        }
        let mut library = self.store.load_library(id)?;
        library.name = new_name;
        library.modified_at = Utc::now();
        self.store.save_library(&library)?;
        Ok(library)
    }

    /// Move a library to a new parent. Validates that no cycle is created.
    pub fn move_library(
        &mut self,
        id: LibraryId,
        new_parent: Option<LibraryId>,
    ) -> Result<Library, BrittleError> {
        // Verify new parent exists if provided.
        if let Some(pid) = new_parent {
            self.store.load_library(pid)?;
        }

        // Cycle detection: walk from new_parent up to root.
        // If we encounter `id` in the ancestry chain, moving would create a cycle.
        if let Some(pid) = new_parent {
            let mut current = pid;
            loop {
                if current == id {
                    return Err(ValidationError::LibraryCycle {
                        library_id: id.to_string(),
                        parent_id: pid.to_string(),
                    }
                    .into());
                }
                let lib = self.store.load_library(current).map_err(|_| {
                    // Broken ancestor chain — treat as no cycle.
                    BrittleError::Validation(ValidationError::LibraryCycle {
                        library_id: id.to_string(),
                        parent_id: pid.to_string(),
                    })
                });
                match lib {
                    Ok(l) => match l.parent_id {
                        Some(next) => current = next,
                        None => break,
                    },
                    Err(_) => break,
                }
            }
        }

        let mut library = self.store.load_library(id)?;
        library.parent_id = new_parent;
        library.modified_at = Utc::now();
        self.store.save_library(&library)?;
        Ok(library)
    }

    /// Delete a library. Fails if it has children. References are disassociated, not deleted.
    pub fn delete_library(&mut self, id: LibraryId) -> Result<(), BrittleError> {
        // Ensure it exists.
        self.store.load_library(id)?;

        // Reject if any library has this one as its parent.
        for lib_id in self.store.list_library_ids()? {
            if lib_id == id {
                continue;
            }
            let child = self.store.load_library(lib_id)?;
            if child.parent_id == Some(id) {
                return Err(ValidationError::LibraryHasChildren { id: id.to_string() }.into());
            }
        }

        self.store.delete_library(id)?;
        Ok(())
    }

    /// List all root libraries (those with no parent).
    pub fn list_root_libraries(&self) -> Result<Vec<Library>, BrittleError> {
        self.list_libraries_where(|lib| lib.parent_id.is_none())
    }

    /// List direct children of a library.
    pub fn list_child_libraries(&self, parent_id: LibraryId) -> Result<Vec<Library>, BrittleError> {
        // Ensure parent exists.
        self.store.load_library(parent_id)?;
        self.list_libraries_where(|lib| lib.parent_id == Some(parent_id))
    }

    fn list_libraries_where(
        &self,
        predicate: impl Fn(&Library) -> bool,
    ) -> Result<Vec<Library>, BrittleError> {
        let ids = self.store.list_library_ids()?;
        let mut libs = Vec::new();
        for id in ids {
            let lib = self.store.load_library(id)?;
            if predicate(&lib) {
                libs.push(lib);
            }
        }
        Ok(libs)
    }

    // ---- Library Membership ----

    /// Add a reference to a library.
    pub fn add_to_library(
        &mut self,
        library_id: LibraryId,
        reference_id: ReferenceId,
    ) -> Result<(), BrittleError> {
        // Verify both exist.
        self.store.load_reference(reference_id)?;
        let mut library = self.store.load_library(library_id)?;
        library.members.insert(reference_id);
        library.modified_at = Utc::now();
        self.store.save_library(&library)?;
        Ok(())
    }

    /// Remove a reference from a library.
    pub fn remove_from_library(
        &mut self,
        library_id: LibraryId,
        reference_id: ReferenceId,
    ) -> Result<(), BrittleError> {
        let mut library = self.store.load_library(library_id)?;
        library.members.remove(&reference_id);
        library.modified_at = Utc::now();
        self.store.save_library(&library)?;
        Ok(())
    }

    /// List all references directly in a library as lightweight summaries.
    pub fn list_library_references(
        &self,
        library_id: LibraryId,
    ) -> Result<Vec<ReferenceSummary>, BrittleError> {
        let library = self.store.load_library(library_id)?;
        let mut summaries = Vec::new();
        for ref_id in &library.members {
            let r = self.store.load_reference(*ref_id)?;
            summaries.push(ReferenceSummary::from(&r));
        }
        Ok(summaries)
    }

    /// List all references in a library and all its descendant libraries.
    ///
    /// De-duplicates references that appear in multiple sub-libraries.
    pub fn list_library_references_recursive(
        &self,
        library_id: LibraryId,
    ) -> Result<Vec<ReferenceSummary>, BrittleError> {
        use std::collections::BTreeSet;
        let mut ref_ids: BTreeSet<ReferenceId> = BTreeSet::new();
        let mut queue = vec![library_id];
        while let Some(id) = queue.pop() {
            let lib = self.store.load_library(id)?;
            ref_ids.extend(lib.members.iter().copied());
            let children = self.list_child_libraries(id)?;
            queue.extend(children.into_iter().map(|c| c.id));
        }
        let mut summaries = Vec::new();
        for ref_id in ref_ids {
            summaries.push(ReferenceSummary::from(&self.store.load_reference(ref_id)?));
        }
        Ok(summaries)
    }

    /// List all libraries that contain a given reference.
    pub fn list_reference_libraries(
        &self,
        reference_id: ReferenceId,
    ) -> Result<Vec<Library>, BrittleError> {
        // Ensure the reference exists.
        self.store.load_reference(reference_id)?;
        self.list_libraries_where(|lib| lib.members.contains(&reference_id))
    }

    // ---- Library ancestry & cascade delete ----

    /// Return the ancestor chain of a library, ordered from root down to its
    /// direct parent.
    ///
    /// Returns an empty `Vec` for root libraries (those with no parent).
    ///
    /// Example: given `Root > Science > Physics`, calling this on `Physics`
    /// returns `[Root, Science]`.
    pub fn get_library_ancestors(&self, id: LibraryId) -> Result<Vec<Library>, BrittleError> {
        let mut lib = self.store.load_library(id)?;
        let mut ancestors = Vec::new();
        while let Some(parent_id) = lib.parent_id {
            let parent = self.store.load_library(parent_id)?;
            ancestors.push(parent.clone());
            lib = parent;
        }
        ancestors.reverse();
        Ok(ancestors)
    }

    /// Delete a library and **all its descendants**, disassociating member
    /// references (references are not deleted, only removed from the deleted
    /// libraries).
    ///
    /// Use after confirming with the user. For the safe, rejection-on-children
    /// variant see [`delete_library`].
    pub fn force_delete_library(&mut self, id: LibraryId) -> Result<(), BrittleError> {
        // Ensure the target exists before doing anything.
        self.store.load_library(id)?;

        // Collect the target and all its descendants via BFS.
        let mut to_delete = vec![id];
        let mut i = 0;
        while i < to_delete.len() {
            let current = to_delete[i];
            for lib_id in self.store.list_library_ids()? {
                if to_delete.contains(&lib_id) {
                    continue;
                }
                let lib = self.store.load_library(lib_id)?;
                if lib.parent_id == Some(current) {
                    to_delete.push(lib_id);
                }
            }
            i += 1;
        }

        // Library records hold membership; deleting the record removes
        // memberships for free. References themselves are unaffected.
        for lib_id in to_delete {
            self.store.delete_library(lib_id)?;
        }
        Ok(())
    }

    // ---- BibTeX Export ----

    /// Export a set of references as a BibTeX string.
    /// Returns an error if any referenced ID is not found.
    /// References with missing required fields are skipped; their errors are collected.
    pub fn export_bibtex(
        &self,
        reference_ids: &[ReferenceId],
    ) -> Result<(String, Vec<BibtexError>), BrittleError> {
        let mut refs = Vec::with_capacity(reference_ids.len());
        for &id in reference_ids {
            refs.push(self.store.load_reference(id)?);
        }
        Ok(export_references(&refs))
    }

    /// Export all references in a library as a BibTeX string.
    pub fn export_library_bibtex(
        &self,
        library_id: LibraryId,
    ) -> Result<(String, Vec<BibtexError>), BrittleError> {
        let library = self.store.load_library(library_id)?;
        let ids: Vec<_> = library.members.iter().copied().collect();
        self.export_bibtex(&ids)
    }

    // ---- Annotations ----

    /// Add an annotation to a reference's PDF.
    pub fn create_annotation(
        &mut self,
        reference_id: ReferenceId,
        page: u32,
        annotation_type: AnnotationType,
        content: Option<String>,
    ) -> Result<Annotation, BrittleError> {
        // Ensure the reference exists.
        self.store.load_reference(reference_id)?;

        let mut set = self.store.load_annotations(reference_id)?;
        let mut ann = Annotation::new(reference_id, page, annotation_type);
        ann.content = content;
        set.annotations.push(ann.clone());
        self.store.save_annotations(&set)?;
        Ok(ann)
    }

    /// Get all annotations for a reference.
    pub fn get_annotations(
        &self,
        reference_id: ReferenceId,
    ) -> Result<Vec<Annotation>, BrittleError> {
        Ok(self.store.load_annotations(reference_id)?.annotations)
    }

    /// Update an annotation. Matches by `annotation.id`; fails if not found.
    pub fn update_annotation(
        &mut self,
        mut annotation: Annotation,
    ) -> Result<Annotation, BrittleError> {
        let ref_id = annotation.reference_id;
        let mut set = self.store.load_annotations(ref_id)?;

        let slot = set
            .annotations
            .iter_mut()
            .find(|a| a.id == annotation.id)
            .ok_or_else(|| {
                BrittleError::Store(StoreError::NotFound {
                    entity_type: EntityType::Annotation,
                    id: annotation.id.to_string(),
                })
            })?;

        annotation.created_at = slot.created_at;
        annotation.modified_at = Utc::now();
        *slot = annotation.clone();
        self.store.save_annotations(&set)?;
        Ok(annotation)
    }

    /// Delete an annotation by ID.
    pub fn delete_annotation(
        &mut self,
        reference_id: ReferenceId,
        annotation_id: AnnotationId,
    ) -> Result<(), BrittleError> {
        let mut set = self.store.load_annotations(reference_id)?;
        let before = set.annotations.len();
        set.annotations.retain(|a| a.id != annotation_id);
        if set.annotations.len() == before {
            return Err(StoreError::NotFound {
                entity_type: EntityType::Annotation,
                id: annotation_id.to_string(),
            }
            .into());
        }
        self.store.save_annotations(&set)?;
        Ok(())
    }

    // ---- Snapshots ----

    /// Create a named snapshot of the current state.
    pub fn create_snapshot(&mut self, message: &str) -> Result<Snapshot, BrittleError> {
        Ok(self.store.create_snapshot(message)?)
    }

    /// List all snapshots in reverse chronological order (most recent first).
    pub fn list_snapshots(&self) -> Result<Vec<Snapshot>, BrittleError> {
        Ok(self.store.list_snapshots()?)
    }

    /// Restore to a previous snapshot.
    ///
    /// Returns an error if there are uncommitted changes. Use [`discard_changes`] first.
    pub fn restore_snapshot(&mut self, snapshot_id: &str) -> Result<(), BrittleError> {
        if self.store.has_uncommitted_changes()? {
            return Err(ValidationError::UncommittedChanges.into());
        }
        Ok(self.store.restore_snapshot(snapshot_id)?)
    }

    /// Returns `true` if there are unsaved changes not yet captured in a snapshot.
    pub fn has_uncommitted_changes(&self) -> Result<bool, BrittleError> {
        Ok(self.store.has_uncommitted_changes()?)
    }

    /// Discard all uncommitted changes, reverting to the state of the last snapshot.
    pub fn discard_changes(&mut self) -> Result<(), BrittleError> {
        let snapshots = self.store.list_snapshots()?;
        if let Some(latest) = snapshots.into_iter().next() {
            self.store.restore_snapshot(&latest.id)?;
        }
        Ok(())
    }
}

/// Case-insensitive substring match across key reference fields.
fn reference_matches(r: &Reference, query: &str) -> bool {
    r.cite_key.to_lowercase().contains(query)
        || r.title().is_some_and(|t| t.to_lowercase().contains(query))
        || r.year().is_some_and(|y| y.to_lowercase().contains(query))
        || r.authors
            .iter()
            .any(|a| a.family.to_lowercase().contains(query))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{AnnotationType, Color, TextMarkupType};

    fn make_brittle() -> Brittle<MemoryStore> {
        Brittle::with_store(MemoryStore::new())
    }

    // ---- Reference tests ----

    #[test]
    fn create_and_get_reference() {
        let mut b = make_brittle();
        let r = b.create_reference("doe2024", EntryType::Article).unwrap();
        let fetched = b.get_reference(r.id).unwrap();
        assert_eq!(fetched.cite_key, "doe2024");
    }

    #[test]
    fn empty_cite_key_rejected() {
        let mut b = make_brittle();
        assert!(matches!(
            b.create_reference("", EntryType::Misc).unwrap_err(),
            BrittleError::Validation(ValidationError::EmptyCiteKey)
        ));
    }

    #[test]
    fn duplicate_cite_key_rejected() {
        let mut b = make_brittle();
        b.create_reference("same", EntryType::Article).unwrap();
        assert!(matches!(
            b.create_reference("same", EntryType::Book).unwrap_err(),
            BrittleError::Validation(ValidationError::DuplicateCiteKey { .. })
        ));
    }

    #[test]
    fn delete_reference_cascades_to_libraries_and_annotations() {
        let mut b = make_brittle();
        let r = b.create_reference("cascade2024", EntryType::Misc).unwrap();
        let lib = b.create_library("Test Lib", None).unwrap();
        b.add_to_library(lib.id, r.id).unwrap();

        // Create annotation.
        b.create_annotation(
            r.id,
            0,
            AnnotationType::Note {
                position: Point { x: 0.0, y: 0.0 },
            },
            None,
        )
        .unwrap();

        b.delete_reference(r.id).unwrap();

        // Reference gone.
        assert!(b.get_reference(r.id).is_err());

        // Library no longer contains the reference.
        let lib2 = b.get_library(lib.id).unwrap();
        assert!(!lib2.members.contains(&r.id));

        // Annotations gone.
        let anns = b.get_annotations(r.id).unwrap();
        assert!(anns.is_empty());
    }

    #[test]
    fn set_and_remove_field() {
        let mut b = make_brittle();
        let r = b
            .create_reference("fields2024", EntryType::Article)
            .unwrap();
        b.set_field(r.id, "title", "Great Paper").unwrap();
        let r2 = b.get_reference(r.id).unwrap();
        assert_eq!(r2.fields.get("title").unwrap(), "Great Paper");

        b.remove_field(r.id, "title").unwrap();
        let r3 = b.get_reference(r.id).unwrap();
        assert!(!r3.fields.contains_key("title"));
    }

    // ---- Library tests ----

    #[test]
    fn create_and_list_root_libraries() {
        let mut b = make_brittle();
        b.create_library("Science", None).unwrap();
        b.create_library("Arts", None).unwrap();
        let roots = b.list_root_libraries().unwrap();
        assert_eq!(roots.len(), 2);
    }

    #[test]
    fn empty_library_name_rejected() {
        let mut b = make_brittle();
        assert!(matches!(
            b.create_library("", None).unwrap_err(),
            BrittleError::Validation(ValidationError::EmptyLibraryName)
        ));
    }

    #[test]
    fn nested_libraries_and_children() {
        let mut b = make_brittle();
        let parent = b.create_library("Science", None).unwrap();
        b.create_library("Physics", Some(parent.id)).unwrap();
        b.create_library("Biology", Some(parent.id)).unwrap();
        let children = b.list_child_libraries(parent.id).unwrap();
        assert_eq!(children.len(), 2);
    }

    #[test]
    fn delete_library_with_children_fails() {
        let mut b = make_brittle();
        let parent = b.create_library("Parent", None).unwrap();
        b.create_library("Child", Some(parent.id)).unwrap();
        assert!(matches!(
            b.delete_library(parent.id).unwrap_err(),
            BrittleError::Validation(ValidationError::LibraryHasChildren { .. })
        ));
    }

    #[test]
    fn move_library_cycle_detection() {
        let mut b = make_brittle();
        let a = b.create_library("A", None).unwrap();
        let b_lib = b.create_library("B", Some(a.id)).unwrap();
        // Moving A under B would create A -> B -> A cycle.
        assert!(matches!(
            b.move_library(a.id, Some(b_lib.id)).unwrap_err(),
            BrittleError::Validation(ValidationError::LibraryCycle { .. })
        ));
    }

    // ---- Membership tests ----

    #[test]
    fn multi_membership() {
        let mut b = make_brittle();
        let r = b.create_reference("multi2024", EntryType::Article).unwrap();
        let lib1 = b.create_library("Lib 1", None).unwrap();
        let lib2 = b.create_library("Lib 2", None).unwrap();

        b.add_to_library(lib1.id, r.id).unwrap();
        b.add_to_library(lib2.id, r.id).unwrap();

        let containing = b.list_reference_libraries(r.id).unwrap();
        let ids: Vec<_> = containing.iter().map(|l| l.id).collect();
        assert!(ids.contains(&lib1.id));
        assert!(ids.contains(&lib2.id));
    }

    #[test]
    fn list_library_references() {
        let mut b = make_brittle();
        let r1 = b.create_reference("r1", EntryType::Misc).unwrap();
        let r2 = b.create_reference("r2", EntryType::Misc).unwrap();
        let lib = b.create_library("All", None).unwrap();
        b.add_to_library(lib.id, r1.id).unwrap();
        b.add_to_library(lib.id, r2.id).unwrap();

        let summaries = b.list_library_references(lib.id).unwrap();
        assert_eq!(summaries.len(), 2);
    }

    // ---- BibTeX export tests ----

    #[test]
    fn export_bibtex_valid_reference() {
        let mut b = make_brittle();
        let mut r = b
            .create_reference("turing1950", EntryType::Article)
            .unwrap();
        r.authors.push(Person::new("Turing"));
        r.fields
            .insert("title".into(), "Computing Machinery".into());
        r.fields.insert("journal".into(), "Mind".into());
        r.fields.insert("year".into(), "1950".into());
        b.update_reference(r.clone()).unwrap();

        let (bibtex, errors) = b.export_bibtex(&[r.id]).unwrap();
        assert!(errors.is_empty());
        assert!(bibtex.contains("@article{turing1950,"));
    }

    // ---- Annotation tests ----

    #[test]
    fn create_update_delete_annotation() {
        let mut b = make_brittle();
        let r = b.create_reference("ann2024", EntryType::Misc).unwrap();

        let ann = b
            .create_annotation(
                r.id,
                0,
                AnnotationType::TextMarkup {
                    markup_type: TextMarkupType::Highlight,
                    quads: vec![],
                    color: Color::YELLOW,
                    selected_text: Some("key text".into()),
                },
                Some("Important!".into()),
            )
            .unwrap();

        let anns = b.get_annotations(r.id).unwrap();
        assert_eq!(anns.len(), 1);
        assert_eq!(anns[0].content.as_deref(), Some("Important!"));

        // Update content.
        let mut updated = ann.clone();
        updated.content = Some("Very important!".into());
        b.update_annotation(updated).unwrap();
        let anns2 = b.get_annotations(r.id).unwrap();
        assert_eq!(anns2[0].content.as_deref(), Some("Very important!"));

        // Delete.
        b.delete_annotation(r.id, ann.id).unwrap();
        assert!(b.get_annotations(r.id).unwrap().is_empty());
    }

    // ---- Library ancestors tests ----

    #[test]
    fn ancestors_of_root_library_is_empty() {
        let mut b = make_brittle();
        let root = b.create_library("Root", None).unwrap();
        assert!(b.get_library_ancestors(root.id).unwrap().is_empty());
    }

    #[test]
    fn ancestors_of_child_returns_parent() {
        let mut b = make_brittle();
        let root = b.create_library("Root", None).unwrap();
        let child = b.create_library("Child", Some(root.id)).unwrap();
        let ancestors = b.get_library_ancestors(child.id).unwrap();
        assert_eq!(ancestors.len(), 1);
        assert_eq!(ancestors[0].id, root.id);
    }

    #[test]
    fn ancestors_are_ordered_root_first() {
        let mut b = make_brittle();
        let root = b.create_library("Root", None).unwrap();
        let mid = b.create_library("Mid", Some(root.id)).unwrap();
        let leaf = b.create_library("Leaf", Some(mid.id)).unwrap();

        let ancestors = b.get_library_ancestors(leaf.id).unwrap();
        assert_eq!(ancestors.len(), 2);
        assert_eq!(ancestors[0].id, root.id);
        assert_eq!(ancestors[1].id, mid.id);
    }

    #[test]
    fn ancestors_of_nonexistent_library_returns_error() {
        let b = make_brittle();
        let fake_id = LibraryId::new();
        assert!(b.get_library_ancestors(fake_id).is_err());
    }

    // ---- Force-delete library tests ----

    #[test]
    fn force_delete_removes_library_with_children() {
        let mut b = make_brittle();
        let parent = b.create_library("Parent", None).unwrap();
        let child = b.create_library("Child", Some(parent.id)).unwrap();

        b.force_delete_library(parent.id).unwrap();

        assert!(b.get_library(parent.id).is_err());
        assert!(b.get_library(child.id).is_err());
    }

    #[test]
    fn force_delete_removes_entire_subtree() {
        let mut b = make_brittle();
        let root = b.create_library("Root", None).unwrap();
        let a = b.create_library("A", Some(root.id)).unwrap();
        let b_lib = b.create_library("B", Some(root.id)).unwrap();
        let a1 = b.create_library("A1", Some(a.id)).unwrap();

        b.force_delete_library(root.id).unwrap();

        for id in [root.id, a.id, b_lib.id, a1.id] {
            assert!(b.get_library(id).is_err());
        }
    }

    #[test]
    fn force_delete_leaves_references_intact() {
        let mut b = make_brittle();
        let lib = b.create_library("Lib", None).unwrap();
        let r = b.create_reference("keep2024", EntryType::Misc).unwrap();
        b.add_to_library(lib.id, r.id).unwrap();

        b.force_delete_library(lib.id).unwrap();

        // Library is gone; reference survives.
        assert!(b.get_library(lib.id).is_err());
        assert!(b.get_reference(r.id).is_ok());
    }

    #[test]
    fn force_delete_nonexistent_library_returns_error() {
        let mut b = make_brittle();
        let fake_id = LibraryId::new();
        assert!(b.force_delete_library(fake_id).is_err());
    }

    // ---- Snapshot tests ----

    #[test]
    fn snapshot_workflow() {
        let mut b = make_brittle();
        let r = b.create_reference("snap2024", EntryType::Misc).unwrap();
        let snap = b.create_snapshot("baseline").unwrap();

        b.delete_reference(r.id).unwrap();
        assert!(b.get_reference(r.id).is_err());

        // MemoryStore allows restore without uncommitted-changes check.
        b.store.restore_snapshot(&snap.id).unwrap();
        assert!(b.get_reference(r.id).is_ok());
    }
}
