//! Transaction Coordinator - 2PC implementation for cross-store transactions
//!
//! Provides distributed transaction support across heterogeneous stores
//! using the Two-Phase Commit (2PC) protocol.
//!
//! The transaction log is persisted as an append-only JSONL file (WAL) so that
//! `recover()` can replay it after a crash.

use std::collections::HashMap;
use std::io::{BufRead, Write as IoWrite};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicU64, Ordering}};

use serde::{Serialize, Deserialize};

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

/// Transaction state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransactionState {
    /// Transaction is active and accepting operations
    Active,
    /// Prepare phase started
    Preparing,
    /// All participants prepared successfully
    Prepared,
    /// Commit in progress
    Committing,
    /// Transaction committed successfully
    Committed,
    /// Rollback in progress
    RollingBack,
    /// Transaction rolled back
    Aborted,
}

/// Transaction log entry for recovery (serializable for WAL persistence)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionLogEntry {
    pub tx_id: u64,
    pub state: TransactionState,
    pub participants: Vec<String>,
    pub operations: Vec<(String, WriteOp)>,
    pub timestamp: u64,
}

/// Participant in a distributed transaction
struct TransactionParticipant {
    store_alias: String,
    store: Arc<dyn Store>,
    local_tx: Option<Box<dyn LocalTransaction>>,
    prepared: bool,
}

/// Active transaction data
struct ActiveTransaction {
    id: u64,
    state: TransactionState,
    participants: HashMap<String, TransactionParticipant>,
    operations: Vec<(String, WriteOp)>,
    created_at: u64,
}

/// Transaction coordinator for 2PC
///
/// Supports two modes:
/// - **In-memory** (`new()`): transaction log is kept in a `Vec` — suitable for tests.
/// - **WAL-backed** (`new_with_wal(path)`): every state transition is appended to a
///   JSONL file so that `recover()` can replay it after a crash.
pub struct TransactionCoordinator {
    /// Next transaction ID
    next_tx_id: AtomicU64,
    /// Active transactions
    active_transactions: RwLock<HashMap<u64, ActiveTransaction>>,
    /// In-memory transaction log (always maintained for `recover()` fast-path)
    transaction_log: RwLock<Vec<TransactionLogEntry>>,
    /// Registered stores
    stores: RwLock<HashMap<String, Arc<dyn Store>>>,
    /// Optional WAL file path for durable logging
    wal_path: Option<PathBuf>,
    /// WAL writer (append-only, flushed after every entry)
    wal_writer: Mutex<Option<std::io::BufWriter<std::fs::File>>>,
}

impl TransactionCoordinator {
    /// Create an in-memory-only transaction coordinator (for tests / ephemeral usage).
    pub fn new() -> Self {
        Self {
            next_tx_id: AtomicU64::new(1),
            active_transactions: RwLock::new(HashMap::new()),
            transaction_log: RwLock::new(Vec::new()),
            stores: RwLock::new(HashMap::new()),
            wal_path: None,
            wal_writer: Mutex::new(None),
        }
    }
    
    /// Create a WAL-backed transaction coordinator.
    ///
    /// If the WAL file already exists it is replayed into the in-memory log
    /// so that `recover()` can detect incomplete transactions.
    pub fn new_with_wal(wal_path: impl AsRef<Path>) -> Result<Self> {
        let wal_path = wal_path.as_ref().to_path_buf();
        
        // Ensure parent directory exists
        if let Some(parent) = wal_path.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|e| FederationError::StorageError(
                    format!("Failed to create WAL directory: {}", e)
                ))?;
        }
        
        // Replay existing WAL entries into memory and determine max tx_id
        let (entries, max_tx_id) = Self::replay_wal(&wal_path)?;
        
        // Open file in append mode
        let file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&wal_path)
            .map_err(|e| FederationError::StorageError(
                format!("Failed to open WAL file: {}", e)
            ))?;
        
        Ok(Self {
            next_tx_id: AtomicU64::new(max_tx_id + 1),
            active_transactions: RwLock::new(HashMap::new()),
            transaction_log: RwLock::new(entries),
            stores: RwLock::new(HashMap::new()),
            wal_path: Some(wal_path),
            wal_writer: Mutex::new(Some(std::io::BufWriter::new(file))),
        })
    }
    
    /// Replay WAL file, returning entries and the maximum tx_id seen.
    fn replay_wal(path: &Path) -> Result<(Vec<TransactionLogEntry>, u64)> {
        if !path.exists() {
            return Ok((Vec::new(), 0));
        }
        
        let file = std::fs::File::open(path)
            .map_err(|e| FederationError::StorageError(
                format!("Failed to open WAL for replay: {}", e)
            ))?;
        let reader = std::io::BufReader::new(file);
        
        let mut entries = Vec::new();
        let mut max_tx_id: u64 = 0;
        
        for line in reader.lines() {
            let line = line.map_err(|e| FederationError::StorageError(
                format!("Failed to read WAL line: {}", e)
            ))?;
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            match serde_json::from_str::<TransactionLogEntry>(trimmed) {
                Ok(entry) => {
                    if entry.tx_id > max_tx_id {
                        max_tx_id = entry.tx_id;
                    }
                    entries.push(entry);
                }
                Err(e) => {
                    // Tolerate corrupt tail (crash mid-write)
                    eprintln!("[TransactionCoordinator] skipping corrupt WAL entry: {}", e);
                    break;
                }
            }
        }
        
        Ok((entries, max_tx_id))
    }
    
    /// Truncate the WAL file (e.g. after successful checkpoint / compaction).
    pub fn truncate_wal(&self) -> Result<()> {
        if let Some(ref wal_path) = self.wal_path {
            let mut writer_guard = self.wal_writer.lock()
                .map_err(|_| FederationError::LockError("WAL writer lock".to_string()))?;
            
            // Truncate file
            let file = std::fs::OpenOptions::new()
                .create(true)
                .write(true)
                .truncate(true)
                .open(wal_path)
                .map_err(|e| FederationError::StorageError(
                    format!("Failed to truncate WAL: {}", e)
                ))?;
            
            *writer_guard = Some(std::io::BufWriter::new(file));
            
            // Clear in-memory log
            let mut log = self.transaction_log.write()
                .map_err(|_| FederationError::LockError("transaction log lock".to_string()))?;
            log.clear();
        }
        Ok(())
    }
    
    /// Register a store for transaction participation
    pub fn register_store(&self, alias: &str, store: Arc<dyn Store>) -> Result<()> {
        let mut stores = self.stores.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        stores.insert(alias.to_string(), store);
        Ok(())
    }
    
    /// Begin a new transaction
    pub fn begin(&self) -> Result<u64> {
        let tx_id = self.next_tx_id.fetch_add(1, Ordering::SeqCst);
        
        let tx = ActiveTransaction {
            id: tx_id,
            state: TransactionState::Active,
            participants: HashMap::new(),
            operations: Vec::new(),
            created_at: Self::current_timestamp(),
        };
        
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        transactions.insert(tx_id, tx);
        
        // Log transaction start
        self.log_transaction(tx_id, TransactionState::Active, vec![], vec![])?;
        
        Ok(tx_id)
    }
    
    /// Add an operation to a transaction
    pub fn add_operation(&self, tx_id: u64, store_alias: &str, op: WriteOp) -> Result<()> {
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        let tx = transactions.get_mut(&tx_id)
            .ok_or(FederationError::TransactionError(format!("Transaction {} not found", tx_id)))?;
        
        if tx.state != TransactionState::Active {
            return Err(FederationError::TransactionError(
                format!("Transaction {} is not active (state: {:?})", tx_id, tx.state)
            ));
        }
        
        // Ensure participant is registered
        if !tx.participants.contains_key(store_alias) {
            let stores = self.stores.read()
                .map_err(|_| FederationError::LockError("Failed to acquire read lock".to_string()))?;
            
            let store = stores.get(store_alias)
                .ok_or(FederationError::StoreNotFound(store_alias.to_string()))?
                .clone();
            
            tx.participants.insert(store_alias.to_string(), TransactionParticipant {
                store_alias: store_alias.to_string(),
                store,
                local_tx: None,
                prepared: false,
            });
        }
        
        tx.operations.push((store_alias.to_string(), op));
        
        Ok(())
    }
    
    /// Commit a transaction using 2PC
    pub fn commit(&self, tx_id: u64) -> Result<()> {
        // Phase 1: Prepare
        self.prepare(tx_id)?;
        
        // Phase 2: Commit
        self.do_commit(tx_id)
    }
    
    /// Rollback a transaction
    pub fn rollback(&self, tx_id: u64) -> Result<()> {
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        let tx = transactions.get_mut(&tx_id)
            .ok_or(FederationError::TransactionError(format!("Transaction {} not found", tx_id)))?;
        
        tx.state = TransactionState::RollingBack;
        
        // Log rollback start
        let participants: Vec<String> = tx.participants.keys().cloned().collect();
        drop(transactions);
        self.log_transaction(tx_id, TransactionState::RollingBack, participants.clone(), vec![])?;
        
        // Rollback all participants
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        if let Some(tx) = transactions.get_mut(&tx_id) {
            for (_, participant) in &mut tx.participants {
                if let Some(mut local_tx) = participant.local_tx.take() {
                    let _ = local_tx.rollback();
                }
            }
            
            tx.state = TransactionState::Aborted;
        }
        
        // Log completion
        self.log_transaction(tx_id, TransactionState::Aborted, participants, vec![])?;
        
        // Remove from active transactions
        transactions.remove(&tx_id);
        
        Ok(())
    }
    
    /// Recover from crash — call on startup **after** `register_store()`.
    ///
    /// Scans the in-memory log (populated from the WAL during `new_with_wal`)
    /// and determines the terminal state of each transaction:
    ///
    /// | Terminal state | Recovery action |
    /// |---|---|
    /// | `Committing` | **Redo** — re-apply stored operations via `store.batch_write()` |
    /// | `Prepared`   | **Undo** — conservative abort (participants may have prepared data) |
    /// | `Active` / `Preparing` / `RollingBack` | **Undo** — discard |
    /// | `Committed` / `Aborted` | No action needed |
    ///
    /// After recovery, a new log entry is written for each resolved transaction
    /// so that subsequent calls are idempotent.
    pub fn recover(&self) -> Result<RecoveryResult> {
        // ── 1. Aggregate WAL: find terminal state + operations for each tx ──

        // A lightweight snapshot so we don't hold the lock during execution.
        struct TxSummary {
            state: TransactionState,
            participants: Vec<String>,
            operations: Vec<(String, WriteOp)>,
        }

        let summaries: HashMap<u64, TxSummary> = {
            let log = self.transaction_log.read()
                .map_err(|_| FederationError::LockError("log read".to_string()))?;

            let mut map: HashMap<u64, TxSummary> = HashMap::new();

            for entry in log.iter() {
                let s = map.entry(entry.tx_id).or_insert_with(|| TxSummary {
                    state: entry.state,
                    participants: Vec::new(),
                    operations: Vec::new(),
                });
                // Each newer entry overwrites the state (WAL is append-only)
                s.state = entry.state;
                if !entry.participants.is_empty() {
                    s.participants = entry.participants.clone();
                }
                if !entry.operations.is_empty() {
                    s.operations = entry.operations.clone();
                }
            }
            map
        };
        // log lock released here

        // ── 2. Classify transactions into redo / undo / skip ────────────────
        let mut redo_txs: Vec<u64> = Vec::new();   // Committing → replay ops
        let mut undo_txs: Vec<u64> = Vec::new();   // Active/Prepared/etc → abort

        for (&tx_id, summary) in &summaries {
            match summary.state {
                TransactionState::Committing => redo_txs.push(tx_id),
                TransactionState::Prepared
                | TransactionState::Active
                | TransactionState::Preparing
                | TransactionState::RollingBack => undo_txs.push(tx_id),
                TransactionState::Committed | TransactionState::Aborted => {
                    // Already terminal — nothing to do
                }
            }
        }

        let mut result = RecoveryResult::default();

        // ── 3. Redo: re-apply operations for Committing transactions ────────
        {
            let stores = self.stores.read()
                .map_err(|_| FederationError::LockError("stores read".to_string()))?;

            for tx_id in &redo_txs {
                let summary = &summaries[tx_id];

                // Group operations by store
                let mut ops_by_store: HashMap<String, Vec<WriteOp>> = HashMap::new();
                for (alias, op) in &summary.operations {
                    ops_by_store.entry(alias.clone()).or_default().push(op.clone());
                }

                let mut redo_ok = true;
                for (alias, ops) in &ops_by_store {
                    if let Some(store) = stores.get(alias) {
                        if let Err(e) = store.batch_write(ops.clone()) {
                            eprintln!("[Recovery] Redo failed for tx {} store {}: {:?}", tx_id, alias, e);
                            redo_ok = false;
                        }
                    } else {
                        eprintln!("[Recovery] Store '{}' not registered, cannot redo tx {}", alias, tx_id);
                        redo_ok = false;
                    }
                }

                if redo_ok {
                    result.completed.push(*tx_id);
                } else {
                    // Redo failed → treat as abort
                    result.aborted.push(*tx_id);
                }
            }
        }
        // stores lock released

        // ── 4. Undo: mark as aborted ────────────────────────────────────────
        for tx_id in &undo_txs {
            result.aborted.push(*tx_id);
        }

        // ── 5. Persist terminal states so recover() is idempotent ───────────
        for &tx_id in &result.completed {
            if let Some(s) = summaries.get(&tx_id) {
                self.log_transaction(tx_id, TransactionState::Committed, s.participants.clone(), vec![])?;
            }
        }
        for &tx_id in &result.aborted {
            if let Some(s) = summaries.get(&tx_id) {
                self.log_transaction(tx_id, TransactionState::Aborted, s.participants.clone(), vec![])?;
            }
        }

        Ok(result)
    }
    
    /// Get transaction state
    pub fn get_state(&self, tx_id: u64) -> Result<Option<TransactionState>> {
        let transactions = self.active_transactions.read()
            .map_err(|_| FederationError::LockError("Failed to acquire read lock".to_string()))?;
        
        Ok(transactions.get(&tx_id).map(|tx| tx.state))
    }
    
    /// List active transactions
    pub fn list_active(&self) -> Result<Vec<u64>> {
        let transactions = self.active_transactions.read()
            .map_err(|_| FederationError::LockError("Failed to acquire read lock".to_string()))?;
        
        Ok(transactions.keys().cloned().collect())
    }
    
    // ===== Private Methods =====
    
    /// Phase 1: Prepare all participants
    fn prepare(&self, tx_id: u64) -> Result<()> {
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        let tx = transactions.get_mut(&tx_id)
            .ok_or(FederationError::TransactionError(format!("Transaction {} not found", tx_id)))?;
        
        if tx.state != TransactionState::Active {
            return Err(FederationError::TransactionError(
                format!("Transaction {} is not active", tx_id)
            ));
        }
        
        tx.state = TransactionState::Preparing;
        
        // Log prepare start
        let participants: Vec<String> = tx.participants.keys().cloned().collect();
        let operations: Vec<(String, WriteOp)> = tx.operations.clone();
        drop(transactions);
        self.log_transaction(tx_id, TransactionState::Preparing, participants.clone(), operations.clone())?;
        
        // Begin local transactions and add operations
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        let tx = transactions.get_mut(&tx_id).unwrap();
        
        // Group operations by store
        let mut ops_by_store: HashMap<String, Vec<WriteOp>> = HashMap::new();
        for (store_alias, op) in &tx.operations {
            ops_by_store.entry(store_alias.clone())
                .or_default()
                .push(op.clone());
        }
        
        // Begin local transactions
        for (store_alias, participant) in &mut tx.participants {
            let local_tx = participant.store.begin_local_tx()?;
            participant.local_tx = Some(local_tx);
        }
        
        // Add operations to local transactions
        for (store_alias, ops) in ops_by_store {
            if let Some(participant) = tx.participants.get_mut(&store_alias) {
                if let Some(local_tx) = &mut participant.local_tx {
                    for op in ops {
                        local_tx.write(op)?;
                    }
                }
            }
        }
        
        // Prepare all participants
        let mut all_prepared = true;
        for (_, participant) in &mut tx.participants {
            if let Some(local_tx) = &mut participant.local_tx {
                match local_tx.prepare() {
                    Ok(true) => {
                        participant.prepared = true;
                    }
                    Ok(false) | Err(_) => {
                        all_prepared = false;
                        break;
                    }
                }
            }
        }
        
        if all_prepared {
            tx.state = TransactionState::Prepared;
            drop(transactions);
            self.log_transaction(tx_id, TransactionState::Prepared, participants, operations)?;
            Ok(())
        } else {
            tx.state = TransactionState::Aborted;
            drop(transactions);
            self.rollback(tx_id)?;
            Err(FederationError::TransactionError(
                format!("Transaction {} prepare failed", tx_id)
            ))
        }
    }
    
    /// Phase 2: Commit all participants
    fn do_commit(&self, tx_id: u64) -> Result<()> {
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        let tx = transactions.get_mut(&tx_id)
            .ok_or(FederationError::TransactionError(format!("Transaction {} not found", tx_id)))?;
        
        if tx.state != TransactionState::Prepared {
            return Err(FederationError::TransactionError(
                format!("Transaction {} is not prepared", tx_id)
            ));
        }
        
        tx.state = TransactionState::Committing;
        
        // Log commit start
        let participants: Vec<String> = tx.participants.keys().cloned().collect();
        drop(transactions);
        self.log_transaction(tx_id, TransactionState::Committing, participants.clone(), vec![])?;
        
        // Commit all participants
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        
        let tx = transactions.get_mut(&tx_id).unwrap();
        
        for (_, participant) in &mut tx.participants {
            if let Some(mut local_tx) = participant.local_tx.take() {
                local_tx.commit()?;
            }
        }
        
        tx.state = TransactionState::Committed;
        
        // Log completion
        drop(transactions);
        self.log_transaction(tx_id, TransactionState::Committed, participants, vec![])?;
        
        // Remove from active transactions
        let mut transactions = self.active_transactions.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        transactions.remove(&tx_id);
        
        Ok(())
    }
    
    fn log_transaction(
        &self,
        tx_id: u64,
        state: TransactionState,
        participants: Vec<String>,
        operations: Vec<(String, WriteOp)>,
    ) -> Result<()> {
        let entry = TransactionLogEntry {
            tx_id,
            state,
            participants,
            operations,
            timestamp: Self::current_timestamp(),
        };
        
        // 1. Append to WAL file (if configured) — fsync for durability
        if self.wal_path.is_some() {
            let mut writer_guard = self.wal_writer.lock()
                .map_err(|_| FederationError::LockError("WAL writer lock".to_string()))?;
            
            if let Some(ref mut writer) = *writer_guard {
                let json = serde_json::to_string(&entry)
                    .map_err(|e| FederationError::StorageError(
                        format!("Failed to serialize WAL entry: {}", e)
                    ))?;
                writeln!(writer, "{}", json)
                    .map_err(|e| FederationError::StorageError(
                        format!("Failed to write WAL entry: {}", e)
                    ))?;
                writer.flush()
                    .map_err(|e| FederationError::StorageError(
                        format!("Failed to flush WAL: {}", e)
                    ))?;
            }
        }
        
        // 2. Append to in-memory log
        let mut log = self.transaction_log.write()
            .map_err(|_| FederationError::LockError("Failed to acquire write lock".to_string()))?;
        log.push(entry);
        
        Ok(())
    }
    
    fn current_timestamp() -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
    }
}

impl Default for TransactionCoordinator {
    fn default() -> Self {
        Self::new()
    }
}

/// Recovery result
#[derive(Debug, Default)]
pub struct RecoveryResult {
    /// Transactions that were completed during recovery
    pub completed: Vec<u64>,
    /// Transactions that were aborted during recovery
    pub aborted: Vec<u64>,
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_transaction_lifecycle() {
        let coordinator = TransactionCoordinator::new();
        
        let tx_id = coordinator.begin().unwrap();
        assert!(tx_id > 0);
        
        let state = coordinator.get_state(tx_id).unwrap();
        assert_eq!(state, Some(TransactionState::Active));
        
        coordinator.rollback(tx_id).unwrap();
        
        let state = coordinator.get_state(tx_id).unwrap();
        assert_eq!(state, None); // Removed after rollback
    }
    
    #[test]
    fn test_multiple_transactions() {
        let coordinator = TransactionCoordinator::new();
        
        let tx1 = coordinator.begin().unwrap();
        let tx2 = coordinator.begin().unwrap();
        let tx3 = coordinator.begin().unwrap();
        
        assert_ne!(tx1, tx2);
        assert_ne!(tx2, tx3);
        
        let active = coordinator.list_active().unwrap();
        assert_eq!(active.len(), 3);
    }
    
    #[test]
    fn test_wal_persistence_and_replay() {
        let temp_dir = std::env::temp_dir().join("aifastdb_test_tx_wal_v2");
        let _ = std::fs::create_dir_all(&temp_dir);
        let wal_path = temp_dir.join("tx.wal");
        let _ = std::fs::remove_file(&wal_path);
        
        // Create coordinator with WAL and begin some transactions
        {
            let coord = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
            let tx1 = coord.begin().unwrap();
            let tx2 = coord.begin().unwrap();
            coord.rollback(tx1).unwrap();
            // tx2 is left active (simulates crash)
            assert!(tx2 > 0);
        }
        
        // WAL file should exist
        assert!(wal_path.exists());
        
        // Replay: create a new coordinator from the same WAL
        {
            let coord2 = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
            let recovery = coord2.recover().unwrap();
            // tx2 was Active at crash → should be in aborted list
            assert!(!recovery.aborted.is_empty(), "Active tx should be aborted on recovery");
            // tx1 was Aborted before crash → should NOT appear
            assert!(recovery.completed.is_empty(), "No completed transactions expected");
        }
        
        // Cleanup
        let _ = std::fs::remove_dir_all(&temp_dir);
    }
    
    #[test]
    fn test_recover_idempotent() {
        // Recover should be idempotent: calling it twice gives same outcome
        let temp_dir = std::env::temp_dir().join("aifastdb_test_recover_idem");
        let _ = std::fs::create_dir_all(&temp_dir);
        let wal_path = temp_dir.join("tx.wal");
        let _ = std::fs::remove_file(&wal_path);
        
        // Simulate crash with an Active transaction
        {
            let coord = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
            coord.begin().unwrap();
            // Drop without commit/rollback
        }
        
        // First recovery
        let coord2 = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
        let r1 = coord2.recover().unwrap();
        assert_eq!(r1.aborted.len(), 1, "Should abort 1 active tx");
        
        // Second recovery on same coordinator → no new aborts
        // (the first recover wrote Aborted entries to the WAL)
        let r2 = coord2.recover().unwrap();
        assert!(r2.aborted.is_empty(), "Second recover should find nothing to do");
        assert!(r2.completed.is_empty());
        
        // Cleanup
        let _ = std::fs::remove_dir_all(&temp_dir);
    }
    
    #[test]
    fn test_recover_committing_redo() {
        // Simulate a crash during commit (state = Committing in WAL)
        let temp_dir = std::env::temp_dir().join("aifastdb_test_recover_redo");
        let _ = std::fs::create_dir_all(&temp_dir);
        let wal_path = temp_dir.join("tx.wal");
        let _ = std::fs::remove_file(&wal_path);
        
        // Manually write a Committing entry to the WAL
        {
            let coord = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
            // Simulate: begin → prepare → committing (then crash)
            coord.log_transaction(
                42,
                TransactionState::Active,
                vec!["store_a".to_string()],
                vec![],
            ).unwrap();
            coord.log_transaction(
                42,
                TransactionState::Prepared,
                vec!["store_a".to_string()],
                vec![
                    ("store_a".to_string(), WriteOp::CreateEntity {
                        entity_type: "person".to_string(),
                        id: "p1".to_string(),
                        name: "Alice".to_string(),
                        attributes: Default::default(),
                        owner_id: None,
                        visibility: None,
                    }),
                ],
            ).unwrap();
            coord.log_transaction(
                42,
                TransactionState::Committing,
                vec!["store_a".to_string()],
                vec![],
            ).unwrap();
        }
        
        // Recover without stores → redo should fail gracefully → abort
        {
            let coord2 = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
            let recovery = coord2.recover().unwrap();
            // No store registered → redo fails → tx is aborted
            assert!(recovery.aborted.contains(&42), "Redo without store should abort");
        }
        
        // Cleanup
        let _ = std::fs::remove_dir_all(&temp_dir);
    }
    
    #[test]
    fn test_wal_truncate() {
        let temp_dir = std::env::temp_dir().join("aifastdb_test_tx_trunc");
        let _ = std::fs::create_dir_all(&temp_dir);
        let wal_path = temp_dir.join("tx.wal");
        let _ = std::fs::remove_file(&wal_path);
        
        let coord = TransactionCoordinator::new_with_wal(&wal_path).unwrap();
        coord.begin().unwrap();
        coord.begin().unwrap();
        
        // Truncate
        coord.truncate_wal().unwrap();
        
        // WAL file should be empty now
        let metadata = std::fs::metadata(&wal_path).unwrap();
        assert_eq!(metadata.len(), 0);
        
        // Cleanup
        let _ = std::fs::remove_dir_all(&temp_dir);
    }
}
