//! Federation Backup & Restore Manager (§8.10)
//!
//! Provides full-snapshot backup and restore for the entire federation:
//! - Per-store data file copy with CRC32 checksum verification
//! - Cross-database index persistence backup
//! - WAL (transaction log) backup
//! - Retention policy enforcement (maxBackups / maxAgeDays)
//!
//! # Design
//!
//! ```text
//! backup_root/
//! └── backup-20260206-153000/
//!     ├── manifest.json          ← BackupManifest (JSON)
//!     ├── stores/
//!     │   ├── knowledge/         ← per-store directory
//!     │   │   └── data.copy      ← recursive copy of store data
//!     │   └── social/
//!     │       └── data.copy
//!     ├── index/
//!     │   └── cross_db_index.json
//!     └── wal/
//!         └── tx.wal
//! ```

use std::collections::HashMap;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

use chrono::Utc;
use serde::{Serialize, Deserialize};

use super::error::{FederationError, Result};
use super::traits::Store;
use super::types::StoreType;

// ---------------------------------------------------------------------------
// Data structures
// ---------------------------------------------------------------------------

/// Backup manifest — serialized to `manifest.json` inside each backup directory.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupManifest {
    /// Unique backup identifier (e.g. `backup-20260206-153000`)
    pub backup_id: String,
    /// ISO-8601 creation timestamp
    pub created_at: String,
    /// Federation engine version that created the backup
    pub federation_version: String,
    /// Per-store backup metadata
    pub stores: Vec<StoreBackupInfo>,
    /// Whether cross-db index was included
    pub include_index: bool,
    /// Whether WAL (transaction log) was included
    pub include_tx_log: bool,
    /// Total bytes across all backed-up artifacts
    pub total_size_bytes: u64,
}

/// Per-store metadata recorded in the manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreBackupInfo {
    /// Store alias
    pub alias: String,
    /// Store type ("Graph" / "Document" / "Vector")
    pub store_type: String,
    /// Relative path inside the backup directory
    pub path: String,
    /// Total size in bytes of the backed-up data
    pub size_bytes: u64,
    /// Number of records at the time of backup
    pub record_count: u64,
    /// CRC32 checksum (hex) computed over the backed-up data
    pub checksum: String,
}

/// Options controlling what and how to back up.
#[derive(Debug, Clone)]
pub struct BackupOptions {
    /// If true, call `sync()` on every store before copying (ensures consistency)
    pub consistent: bool,
    /// Whether to include the cross-db index in the backup
    pub include_index: bool,
    /// Whether to include the WAL transaction log
    pub include_tx_log: bool,
    /// Restrict backup to specific stores (None = all)
    pub stores: Option<Vec<String>>,
}

impl Default for BackupOptions {
    fn default() -> Self {
        Self {
            consistent: true,
            include_index: true,
            include_tx_log: true,
            stores: None,
        }
    }
}

/// Options controlling what to restore.
#[derive(Debug, Clone)]
pub struct RestoreOptions {
    /// Whether to restore the cross-db index
    pub restore_index: bool,
    /// Restrict restore to specific stores (None = all)
    pub stores: Option<Vec<String>>,
}

impl Default for RestoreOptions {
    fn default() -> Self {
        Self {
            restore_index: true,
            stores: None,
        }
    }
}

/// Backup retention policy.
#[derive(Debug, Clone)]
pub struct RetentionPolicy {
    /// Maximum number of backups to keep (oldest are pruned first)
    pub max_backups: usize,
    /// Maximum age in days; backups older than this are pruned
    pub max_age_days: u32,
}

impl Default for RetentionPolicy {
    fn default() -> Self {
        Self {
            max_backups: 5,
            max_age_days: 30,
        }
    }
}

// ---------------------------------------------------------------------------
// FederationBackup
// ---------------------------------------------------------------------------

/// Federation backup manager.
///
/// Operates on file-system level: each registered store must be associated
/// with a data path so that `FederationBackup` can copy files to/from the
/// backup directory.
pub struct FederationBackup {
    /// Root directory where backups are stored
    backup_root: PathBuf,
    /// Mapping: store_alias → (Arc<dyn Store>, data_path)
    stores: HashMap<String, (std::sync::Arc<dyn Store>, PathBuf)>,
    /// Optional cross-db index file path
    index_path: Option<PathBuf>,
    /// Optional WAL file path
    wal_path: Option<PathBuf>,
}

impl FederationBackup {
    /// Create a new FederationBackup manager.
    ///
    /// `backup_root` — directory where all backup snapshots are stored.
    pub fn new(backup_root: impl Into<PathBuf>) -> Self {
        Self {
            backup_root: backup_root.into(),
            stores: HashMap::new(),
            index_path: None,
            wal_path: None,
        }
    }

    /// Register a store with its data path for backup.
    pub fn register_store(
        &mut self,
        alias: &str,
        store: std::sync::Arc<dyn Store>,
        data_path: impl Into<PathBuf>,
    ) {
        self.stores.insert(alias.to_string(), (store, data_path.into()));
    }

    /// Set the cross-database index file path (for backup/restore).
    pub fn set_index_path(&mut self, path: impl Into<PathBuf>) {
        self.index_path = Some(path.into());
    }

    /// Set the WAL file path (for backup/restore).
    pub fn set_wal_path(&mut self, path: impl Into<PathBuf>) {
        self.wal_path = Some(path.into());
    }

    // ===== Backup ============================================================

    /// Create a full-snapshot backup.
    ///
    /// 1. (optional) sync all stores to ensure data is flushed
    /// 2. Copy each store's data directory/file into the backup
    /// 3. Compute CRC32 checksum for each store backup
    /// 4. Copy cross-db index and WAL if requested
    /// 5. Write `manifest.json`
    pub fn create_snapshot(&self, options: &BackupOptions) -> Result<BackupManifest> {
        let backup_id = Self::generate_backup_id();
        let backup_dir = self.backup_root.join(&backup_id);
        std::fs::create_dir_all(&backup_dir)
            .map_err(|e| FederationError::IoError(e))?;

        let mut manifest = BackupManifest {
            backup_id: backup_id.clone(),
            created_at: Utc::now().to_rfc3339(),
            federation_version: env!("CARGO_PKG_VERSION").to_string(),
            stores: Vec::new(),
            include_index: options.include_index,
            include_tx_log: options.include_tx_log,
            total_size_bytes: 0,
        };

        // 1. Sync all stores for consistency
        if options.consistent {
            for (alias, (store, _)) in &self.stores {
                if Self::should_include(alias, &options.stores) {
                    store.sync()?;
                }
            }
        }

        // 2. Backup each store
        let stores_dir = backup_dir.join("stores");
        std::fs::create_dir_all(&stores_dir)
            .map_err(|e| FederationError::IoError(e))?;

        for (alias, (store, data_path)) in &self.stores {
            if !Self::should_include(alias, &options.stores) {
                continue;
            }

            let store_backup_dir = stores_dir.join(alias);
            std::fs::create_dir_all(&store_backup_dir)
                .map_err(|e| FederationError::IoError(e))?;

            let (size_bytes, checksum) = Self::copy_path(data_path, &store_backup_dir)?;
            let stats = store.stats();

            let info = StoreBackupInfo {
                alias: alias.clone(),
                store_type: format!("{:?}", store.store_type()),
                path: format!("stores/{}", alias),
                size_bytes,
                record_count: stats.record_count as u64,
                checksum,
            };
            manifest.total_size_bytes += size_bytes;
            manifest.stores.push(info);
        }

        // 3. Backup cross-db index
        if options.include_index {
            if let Some(ref idx_path) = self.index_path {
                if idx_path.exists() {
                    let index_dir = backup_dir.join("index");
                    std::fs::create_dir_all(&index_dir)
                        .map_err(|e| FederationError::IoError(e))?;
                    let (sz, _) = Self::copy_path(idx_path, &index_dir)?;
                    manifest.total_size_bytes += sz;
                }
            }
        }

        // 4. Backup WAL
        if options.include_tx_log {
            if let Some(ref wal) = self.wal_path {
                if wal.exists() {
                    let wal_dir = backup_dir.join("wal");
                    std::fs::create_dir_all(&wal_dir)
                        .map_err(|e| FederationError::IoError(e))?;
                    let (sz, _) = Self::copy_path(wal, &wal_dir)?;
                    manifest.total_size_bytes += sz;
                }
            }
        }

        // 5. Write manifest
        let manifest_json = serde_json::to_string_pretty(&manifest)
            .map_err(|e| FederationError::SerializationError(e.to_string()))?;
        let manifest_path = backup_dir.join("manifest.json");
        std::fs::write(&manifest_path, manifest_json)
            .map_err(|e| FederationError::IoError(e))?;

        Ok(manifest)
    }

    // ===== List ==============================================================

    /// List all available backups, sorted by creation time (newest first).
    pub fn list_backups(&self) -> Result<Vec<BackupManifest>> {
        if !self.backup_root.exists() {
            return Ok(Vec::new());
        }

        let mut backups = Vec::new();
        let entries = std::fs::read_dir(&self.backup_root)
            .map_err(|e| FederationError::IoError(e))?;

        for entry in entries {
            let entry = entry.map_err(|e| FederationError::IoError(e))?;
            let manifest_path = entry.path().join("manifest.json");
            if manifest_path.exists() {
                let content = std::fs::read_to_string(&manifest_path)
                    .map_err(|e| FederationError::IoError(e))?;
                match serde_json::from_str::<BackupManifest>(&content) {
                    Ok(m) => backups.push(m),
                    Err(_) => {
                        // Skip corrupted manifests
                        continue;
                    }
                }
            }
        }

        // Sort newest first
        backups.sort_by(|a, b| b.created_at.cmp(&a.created_at));
        Ok(backups)
    }

    // ===== Restore ===========================================================

    /// Restore from a previously created backup snapshot.
    ///
    /// 1. Verify CRC32 checksums of all store backups
    /// 2. Copy data back to original store paths
    /// 3. Restore cross-db index if requested
    pub fn restore_snapshot(
        &self,
        backup_id: &str,
        options: &RestoreOptions,
    ) -> Result<()> {
        let backup_dir = self.backup_root.join(backup_id);
        if !backup_dir.exists() {
            return Err(FederationError::StorageError(
                format!("Backup not found: {}", backup_id),
            ));
        }

        let manifest_path = backup_dir.join("manifest.json");
        let content = std::fs::read_to_string(&manifest_path)
            .map_err(|e| FederationError::IoError(e))?;
        let manifest: BackupManifest = serde_json::from_str(&content)
            .map_err(|e| FederationError::SerializationError(e.to_string()))?;

        // 1. Verify checksums
        self.verify_checksums(&manifest, &backup_dir)?;

        // 2. Restore each store
        for store_info in &manifest.stores {
            if let Some(ref targets) = options.stores {
                if !targets.contains(&store_info.alias) {
                    continue;
                }
            }

            // Find the registered store's data path
            let data_path = self.stores.get(&store_info.alias)
                .map(|(_, p)| p.clone())
                .ok_or_else(|| FederationError::StoreNotFound(
                    format!("Store '{}' not registered for restore", store_info.alias),
                ))?;

            let source_dir = backup_dir.join(&store_info.path);
            Self::restore_path(&source_dir, &data_path)?;
        }

        // 3. Restore cross-db index
        if manifest.include_index && options.restore_index {
            if let Some(ref idx_path) = self.index_path {
                let index_dir = backup_dir.join("index");
                if index_dir.exists() {
                    Self::restore_path(&index_dir, idx_path)?;
                }
            }
        }

        Ok(())
    }

    // ===== Verify ============================================================

    /// Verify the CRC32 checksums of all store backups in a manifest.
    pub fn verify_checksums(
        &self,
        manifest: &BackupManifest,
        backup_dir: &Path,
    ) -> Result<()> {
        for store_info in &manifest.stores {
            let store_backup_dir = backup_dir.join(&store_info.path);
            if !store_backup_dir.exists() {
                return Err(FederationError::StorageError(
                    format!("Backup data missing for store '{}'", store_info.alias),
                ));
            }

            let actual_checksum = Self::compute_dir_checksum(&store_backup_dir)?;
            if actual_checksum != store_info.checksum {
                return Err(FederationError::StorageError(format!(
                    "Checksum mismatch for store '{}': expected {}, got {}",
                    store_info.alias, store_info.checksum, actual_checksum
                )));
            }
        }
        Ok(())
    }

    // ===== Delete ============================================================

    /// Delete a specific backup by ID.
    pub fn delete_backup(&self, backup_id: &str) -> Result<()> {
        let backup_dir = self.backup_root.join(backup_id);
        if !backup_dir.exists() {
            return Err(FederationError::StorageError(
                format!("Backup not found: {}", backup_id),
            ));
        }
        std::fs::remove_dir_all(&backup_dir)
            .map_err(|e| FederationError::IoError(e))?;
        Ok(())
    }

    // ===== Retention Policy ===================================================

    /// Apply a retention policy: prune old backups exceeding limits.
    ///
    /// - Deletes backups beyond `max_backups` (oldest first)
    /// - Deletes backups older than `max_age_days`
    ///
    /// Returns the number of backups deleted.
    pub fn apply_retention_policy(&self, policy: &RetentionPolicy) -> Result<usize> {
        let backups = self.list_backups()?;
        // Already sorted newest-first
        let mut deleted = 0usize;

        let now = Utc::now();

        // First pass: delete backups older than max_age_days
        let mut to_keep = Vec::new();
        for backup in backups {
            if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&backup.created_at) {
                let age_days = (now - created.with_timezone(&Utc)).num_days();
                if age_days > policy.max_age_days as i64 {
                    let _ = self.delete_backup(&backup.backup_id);
                    deleted += 1;
                    continue;
                }
            }
            to_keep.push(backup);
        }

        // Second pass: keep only max_backups (newest first, already sorted)
        if to_keep.len() > policy.max_backups {
            let excess = to_keep.split_off(policy.max_backups);
            for backup in excess {
                let _ = self.delete_backup(&backup.backup_id);
                deleted += 1;
            }
        }

        Ok(deleted)
    }

    // ===== Internal helpers ==================================================

    fn generate_backup_id() -> String {
        Utc::now().format("backup-%Y%m%d-%H%M%S").to_string()
    }

    /// Check whether a store alias should be included given the optional filter.
    fn should_include(alias: &str, filter: &Option<Vec<String>>) -> bool {
        match filter {
            None => true,
            Some(list) => list.iter().any(|s| s == alias),
        }
    }

    /// Recursively copy a file or directory from `src` to `dst`.
    ///
    /// Returns `(total_bytes_copied, crc32_hex)`.
    /// The CRC32 is computed over the concatenated content of all files.
    fn copy_path(src: &Path, dst: &Path) -> Result<(u64, String)> {
        let mut hasher = crc32fast::Hasher::new();
        let mut total_bytes = 0u64;

        if src.is_file() {
            let (bytes, _) = Self::copy_file_with_hash(src, dst, &mut hasher)?;
            total_bytes += bytes;
        } else if src.is_dir() {
            std::fs::create_dir_all(dst).map_err(|e| FederationError::IoError(e))?;
            total_bytes += Self::copy_dir_recursive(src, dst, &mut hasher)?;
        }
        // else: source doesn't exist — skip silently

        let checksum = format!("{:08x}", hasher.finalize());
        Ok((total_bytes, checksum))
    }

    /// Copy a single file from `src` to `dst_dir/<filename>`, updating the hasher.
    fn copy_file_with_hash(
        src: &Path,
        dst_dir: &Path,
        hasher: &mut crc32fast::Hasher,
    ) -> Result<(u64, PathBuf)> {
        let file_name = src.file_name()
            .ok_or_else(|| FederationError::StorageError("Invalid file path".to_string()))?;
        let dst_file = dst_dir.join(file_name);

        let mut src_file = std::fs::File::open(src)
            .map_err(|e| FederationError::IoError(e))?;
        let mut dst_f = std::fs::File::create(&dst_file)
            .map_err(|e| FederationError::IoError(e))?;

        let mut buf = [0u8; 8192];
        let mut total = 0u64;
        loop {
            let n = src_file.read(&mut buf)
                .map_err(|e| FederationError::IoError(e))?;
            if n == 0 {
                break;
            }
            hasher.update(&buf[..n]);
            dst_f.write_all(&buf[..n])
                .map_err(|e| FederationError::IoError(e))?;
            total += n as u64;
        }

        Ok((total, dst_file))
    }

    /// Recursively copy all files under `src_dir` into `dst_dir`.
    fn copy_dir_recursive(
        src_dir: &Path,
        dst_dir: &Path,
        hasher: &mut crc32fast::Hasher,
    ) -> Result<u64> {
        let mut total = 0u64;
        let entries = std::fs::read_dir(src_dir)
            .map_err(|e| FederationError::IoError(e))?;

        for entry in entries {
            let entry = entry.map_err(|e| FederationError::IoError(e))?;
            let src_path = entry.path();
            let file_name = entry.file_name();
            let dst_path = dst_dir.join(&file_name);

            if src_path.is_dir() {
                std::fs::create_dir_all(&dst_path)
                    .map_err(|e| FederationError::IoError(e))?;
                total += Self::copy_dir_recursive(&src_path, &dst_path, hasher)?;
            } else if src_path.is_file() {
                let (bytes, _) = Self::copy_file_with_hash(&src_path, dst_dir, hasher)?;
                total += bytes;
            }
        }

        Ok(total)
    }

    /// Compute a CRC32 checksum for all files under a directory (or single file).
    fn compute_dir_checksum(path: &Path) -> Result<String> {
        let mut hasher = crc32fast::Hasher::new();

        if path.is_file() {
            Self::hash_file(path, &mut hasher)?;
        } else if path.is_dir() {
            Self::hash_dir_recursive(path, &mut hasher)?;
        }

        Ok(format!("{:08x}", hasher.finalize()))
    }

    fn hash_file(path: &Path, hasher: &mut crc32fast::Hasher) -> Result<()> {
        let mut file = std::fs::File::open(path)
            .map_err(|e| FederationError::IoError(e))?;
        let mut buf = [0u8; 8192];
        loop {
            let n = file.read(&mut buf)
                .map_err(|e| FederationError::IoError(e))?;
            if n == 0 {
                break;
            }
            hasher.update(&buf[..n]);
        }
        Ok(())
    }

    fn hash_dir_recursive(dir: &Path, hasher: &mut crc32fast::Hasher) -> Result<()> {
        // Sort entries for deterministic checksum order
        let mut entries: Vec<_> = std::fs::read_dir(dir)
            .map_err(|e| FederationError::IoError(e))?
            .filter_map(|e| e.ok())
            .collect();
        entries.sort_by_key(|e| e.file_name());

        for entry in entries {
            let path = entry.path();
            if path.is_dir() {
                Self::hash_dir_recursive(&path, hasher)?;
            } else if path.is_file() {
                Self::hash_file(&path, hasher)?;
            }
        }
        Ok(())
    }

    /// Restore: copy files from backup source to target destination.
    ///
    /// If `dst` is a file, copies the single matching file from `src_dir`.
    /// If `dst` is a directory (or doesn't exist yet), recursively copies.
    fn restore_path(src_dir: &Path, dst: &Path) -> Result<()> {
        if !src_dir.exists() {
            return Err(FederationError::StorageError(
                format!("Backup source path does not exist: {:?}", src_dir),
            ));
        }

        // If dst already exists as a directory, clear it first for clean restore
        if dst.is_dir() {
            // Remove existing content but keep the directory
            let entries = std::fs::read_dir(dst)
                .map_err(|e| FederationError::IoError(e))?;
            for entry in entries {
                let entry = entry.map_err(|e| FederationError::IoError(e))?;
                let path = entry.path();
                if path.is_dir() {
                    std::fs::remove_dir_all(&path)
                        .map_err(|e| FederationError::IoError(e))?;
                } else {
                    std::fs::remove_file(&path)
                        .map_err(|e| FederationError::IoError(e))?;
                }
            }
        }

        // Copy from backup to target
        if src_dir.is_dir() {
            if !dst.exists() {
                std::fs::create_dir_all(dst)
                    .map_err(|e| FederationError::IoError(e))?;
            }
            Self::copy_dir_simple(src_dir, dst)?;
        } else if src_dir.is_file() {
            if let Some(parent) = dst.parent() {
                std::fs::create_dir_all(parent)
                    .map_err(|e| FederationError::IoError(e))?;
            }
            std::fs::copy(src_dir, dst)
                .map_err(|e| FederationError::IoError(e))?;
        }

        Ok(())
    }

    /// Simple recursive directory copy (without checksum computation).
    fn copy_dir_simple(src: &Path, dst: &Path) -> Result<()> {
        let entries = std::fs::read_dir(src)
            .map_err(|e| FederationError::IoError(e))?;

        for entry in entries {
            let entry = entry.map_err(|e| FederationError::IoError(e))?;
            let src_path = entry.path();
            let dst_path = dst.join(entry.file_name());

            if src_path.is_dir() {
                std::fs::create_dir_all(&dst_path)
                    .map_err(|e| FederationError::IoError(e))?;
                Self::copy_dir_simple(&src_path, &dst_path)?;
            } else {
                std::fs::copy(&src_path, &dst_path)
                    .map_err(|e| FederationError::IoError(e))?;
            }
        }

        Ok(())
    }
}

// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    /// Helper: create a temporary directory with some test files.
    fn create_test_store_dir(base: &Path, alias: &str) -> PathBuf {
        let store_dir = base.join(alias);
        std::fs::create_dir_all(&store_dir).unwrap();
        std::fs::write(store_dir.join("data.jsonl"), b"line1\nline2\nline3\n").unwrap();
        std::fs::write(store_dir.join("index.bin"), b"FAKE_INDEX_DATA").unwrap();
        store_dir
    }

    /// Minimal Store mock for backup tests.
    struct MockStore {
        alias: String,
        record_count: usize,
    }

    impl super::super::traits::AsAny for MockStore {
        fn as_any(&self) -> &dyn std::any::Any { self }
        fn as_any_arc(self: Arc<Self>) -> Arc<dyn std::any::Any + Send + Sync> { self }
    }

    impl Store for MockStore {
        fn store_type(&self) -> StoreType { StoreType::Document }
        fn alias(&self) -> &str { &self.alias }
        fn query(&self, _q: &super::super::types::UniversalQuery) -> Result<Vec<super::super::types::Record>> { Ok(vec![]) }
        fn write(&self, _op: super::super::types::WriteOp) -> Result<super::super::types::WriteResult> {
            Ok(super::super::types::WriteResult::NoOp)
        }
        fn batch_write(&self, _ops: Vec<super::super::types::WriteOp>) -> Result<super::super::types::BatchWriteResult> {
            Ok(super::super::types::BatchWriteResult::default())
        }
        fn begin_local_tx(&self) -> Result<Box<dyn super::super::traits::LocalTransaction + Send>> {
            Err(FederationError::UnsupportedOperation("mock".into()))
        }
        fn get(&self, _id: &str) -> Result<Option<super::super::types::Record>> { Ok(None) }
        fn exists(&self, _id: &str) -> Result<bool> { Ok(false) }
        fn query_iter(&self, _q: &super::super::types::UniversalQuery) -> Result<Box<dyn Iterator<Item = Result<super::super::types::Record>> + Send>> {
            Ok(Box::new(std::iter::empty()))
        }
        fn stats(&self) -> super::super::traits::StoreStats {
            super::super::traits::StoreStats {
                record_count: self.record_count,
                ..Default::default()
            }
        }
        fn update_ownership(&self, _id: &str, _ownership: super::super::types::OwnershipMeta) -> Result<()> { Ok(()) }
        fn sync(&self) -> Result<()> { Ok(()) }
        fn close(&self) -> Result<()> { Ok(()) }
    }

    #[test]
    fn test_create_and_list_backup() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_create");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_data = create_test_store_dir(&temp, "knowledge");

        let mock = Arc::new(MockStore { alias: "knowledge".into(), record_count: 3 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("knowledge", mock, &store_data);

        let manifest = mgr.create_snapshot(&BackupOptions {
            consistent: true,
            include_index: false,
            include_tx_log: false,
            stores: None,
        }).unwrap();

        assert_eq!(manifest.stores.len(), 1);
        assert_eq!(manifest.stores[0].alias, "knowledge");
        assert_eq!(manifest.stores[0].record_count, 3);
        assert!(manifest.total_size_bytes > 0);
        assert!(!manifest.stores[0].checksum.is_empty());

        // List should return exactly 1 backup
        let list = mgr.list_backups().unwrap();
        assert_eq!(list.len(), 1);
        assert_eq!(list[0].backup_id, manifest.backup_id);

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_backup_and_restore() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_restore");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_data = create_test_store_dir(&temp, "docs");

        let mock = Arc::new(MockStore { alias: "docs".into(), record_count: 2 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("docs", mock, &store_data);

        // Create backup
        let manifest = mgr.create_snapshot(&BackupOptions::default()).unwrap();

        // Modify original data
        std::fs::write(store_data.join("data.jsonl"), b"MODIFIED").unwrap();
        let modified = std::fs::read_to_string(store_data.join("data.jsonl")).unwrap();
        assert_eq!(modified, "MODIFIED");

        // Restore from backup
        mgr.restore_snapshot(&manifest.backup_id, &RestoreOptions::default()).unwrap();

        // Verify data is restored
        let restored = std::fs::read_to_string(store_data.join("data.jsonl")).unwrap();
        assert_eq!(restored, "line1\nline2\nline3\n");
        let idx = std::fs::read_to_string(store_data.join("index.bin")).unwrap();
        assert_eq!(idx, "FAKE_INDEX_DATA");

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_verify_checksum_detects_corruption() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_corrupt");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_data = create_test_store_dir(&temp, "graph");

        let mock = Arc::new(MockStore { alias: "graph".into(), record_count: 1 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("graph", mock, &store_data);

        let manifest = mgr.create_snapshot(&BackupOptions {
            include_index: false,
            include_tx_log: false,
            ..BackupOptions::default()
        }).unwrap();

        // Corrupt the backup data
        let backup_data_file = backup_root
            .join(&manifest.backup_id)
            .join("stores")
            .join("graph")
            .join("data.jsonl");
        std::fs::write(&backup_data_file, b"CORRUPTED_DATA").unwrap();

        // Verify should fail
        let backup_dir = backup_root.join(&manifest.backup_id);
        let result = mgr.verify_checksums(&manifest, &backup_dir);
        assert!(result.is_err());

        let err_msg = format!("{}", result.unwrap_err());
        assert!(err_msg.contains("Checksum mismatch"));

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_retention_policy() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_retention");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_data = create_test_store_dir(&temp, "data");

        let mock = Arc::new(MockStore { alias: "data".into(), record_count: 1 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("data", mock, &store_data);

        let opts = BackupOptions {
            include_index: false,
            include_tx_log: false,
            ..BackupOptions::default()
        };

        // Create 4 backups (with slight delay to ensure unique IDs)
        let mut ids = Vec::new();
        for i in 0..4 {
            // Create backups with distinct timestamps in the directory name
            let manifest = mgr.create_snapshot(&opts).unwrap();
            ids.push(manifest.backup_id.clone());

            // Rename to give distinct IDs (since they'd all have the same second)
            let new_id = format!("backup-20260206-00000{}", i);
            let old_dir = backup_root.join(&manifest.backup_id);
            let new_dir = backup_root.join(&new_id);
            if old_dir != new_dir {
                std::fs::rename(&old_dir, &new_dir).unwrap();
                // Update manifest.json inside
                let mf_path = new_dir.join("manifest.json");
                let content = std::fs::read_to_string(&mf_path).unwrap();
                let mut m: BackupManifest = serde_json::from_str(&content).unwrap();
                m.backup_id = new_id.clone();
                std::fs::write(&mf_path, serde_json::to_string_pretty(&m).unwrap()).unwrap();
            }
        }

        let list = mgr.list_backups().unwrap();
        assert_eq!(list.len(), 4);

        // Apply retention: keep max 2
        let deleted = mgr.apply_retention_policy(&RetentionPolicy {
            max_backups: 2,
            max_age_days: 365,
        }).unwrap();
        assert_eq!(deleted, 2);

        let remaining = mgr.list_backups().unwrap();
        assert_eq!(remaining.len(), 2);

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_delete_backup() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_delete");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_data = create_test_store_dir(&temp, "s1");

        let mock = Arc::new(MockStore { alias: "s1".into(), record_count: 0 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("s1", mock, &store_data);

        let manifest = mgr.create_snapshot(&BackupOptions {
            include_index: false,
            include_tx_log: false,
            ..BackupOptions::default()
        }).unwrap();

        assert_eq!(mgr.list_backups().unwrap().len(), 1);
        mgr.delete_backup(&manifest.backup_id).unwrap();
        assert_eq!(mgr.list_backups().unwrap().len(), 0);

        // Deleting non-existent should error
        assert!(mgr.delete_backup("nonexistent").is_err());

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_restore_nonexistent_backup() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_noexist");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let mgr = FederationBackup::new(temp.join("backups"));
        let result = mgr.restore_snapshot("backup-does-not-exist", &RestoreOptions::default());
        assert!(result.is_err());
        let err = format!("{}", result.unwrap_err());
        assert!(err.contains("Backup not found"));

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_selective_store_backup() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_selective");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_a = create_test_store_dir(&temp, "storeA");
        let store_b = create_test_store_dir(&temp, "storeB");

        let mock_a = Arc::new(MockStore { alias: "storeA".into(), record_count: 5 });
        let mock_b = Arc::new(MockStore { alias: "storeB".into(), record_count: 10 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("storeA", mock_a, &store_a);
        mgr.register_store("storeB", mock_b, &store_b);

        // Backup only storeA
        let manifest = mgr.create_snapshot(&BackupOptions {
            stores: Some(vec!["storeA".to_string()]),
            include_index: false,
            include_tx_log: false,
            ..BackupOptions::default()
        }).unwrap();

        assert_eq!(manifest.stores.len(), 1);
        assert_eq!(manifest.stores[0].alias, "storeA");

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }

    #[test]
    fn test_index_and_wal_backup() {
        let temp = std::env::temp_dir().join("aifastdb_test_backup_idx_wal");
        let _ = std::fs::remove_dir_all(&temp);
        std::fs::create_dir_all(&temp).unwrap();

        let backup_root = temp.join("backups");
        let store_data = create_test_store_dir(&temp, "main");

        // Create fake index and WAL files
        let index_file = temp.join("cross_db_index.json");
        std::fs::write(&index_file, b"{\"entities\":{}}").unwrap();
        let wal_file = temp.join("tx.wal");
        std::fs::write(&wal_file, b"WAL_ENTRY_1\nWAL_ENTRY_2\n").unwrap();

        let mock = Arc::new(MockStore { alias: "main".into(), record_count: 1 });

        let mut mgr = FederationBackup::new(&backup_root);
        mgr.register_store("main", mock, &store_data);
        mgr.set_index_path(&index_file);
        mgr.set_wal_path(&wal_file);

        let manifest = mgr.create_snapshot(&BackupOptions {
            include_index: true,
            include_tx_log: true,
            ..BackupOptions::default()
        }).unwrap();

        assert!(manifest.include_index);
        assert!(manifest.include_tx_log);

        // Verify backup contains index and wal directories
        let backup_dir = backup_root.join(&manifest.backup_id);
        assert!(backup_dir.join("index").exists());
        assert!(backup_dir.join("wal").exists());

        // Cleanup
        let _ = std::fs::remove_dir_all(&temp);
    }
}
