/// Errors originating from WAL operations.
///
/// Covers I/O failures, data corruption detected during recovery,
/// and lifecycle violations (e.g., appending after shutdown).
#[derive(Debug, thiserror::Error)]
pub enum WalError {
    /// Underlying filesystem I/O failure.
    #[error("WAL I/O error: {0}")]
    Io(#[from] std::io::Error),
    /// Data corruption detected (BLAKE3 mismatch, invalid magic, etc.).
    #[error("WAL corruption: {message}")]
    Corruption { message: String },
    /// Invalid WAL configuration supplied at open time.
    ///
    /// Returned (rather than panicking or crashing the writer thread) when a
    /// configuration value would make durable writes impossible — e.g. a
    /// `batch_size` larger than the wire format's `MAX_EVENTS_PER_BATCH`, which
    /// would otherwise fail every batch encode and permanently wedge the
    /// writer. Surfacing it to the caller of `open()` lets the embedding
    /// process fail loudly at startup instead of silently after the first
    /// full batch.
    #[error("invalid WAL configuration: {message}")]
    InvalidConfig { message: String },
    /// Attempted append after WAL has been shut down.
    #[error("WAL closed")]
    Closed,
    /// Channel send to writer thread failed (writer thread panicked or exited).
    #[error("WAL channel send failed")]
    SendFailed,
    /// Writer thread join failed during shutdown.
    #[error("WAL shutdown failed")]
    ShutdownFailed,
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn display_io() {
        let e = WalError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "file not found",
        ));
        assert!(e.to_string().contains("I/O error"));
        assert!(e.to_string().contains("file not found"));
    }

    #[test]
    fn display_corruption() {
        let e = WalError::Corruption {
            message: "bad checksum".into(),
        };
        assert_eq!(e.to_string(), "WAL corruption: bad checksum");
    }

    #[test]
    fn display_closed() {
        assert_eq!(WalError::Closed.to_string(), "WAL closed");
    }

    #[test]
    fn display_invalid_config() {
        let e = WalError::InvalidConfig {
            message: "batch_size 300 exceeds 256".into(),
        };
        assert_eq!(
            e.to_string(),
            "invalid WAL configuration: batch_size 300 exceeds 256"
        );
    }

    #[test]
    fn source_invalid_config_is_none() {
        use std::error::Error;
        let e = WalError::InvalidConfig {
            message: "x".into(),
        };
        assert!(e.source().is_none());
    }

    #[test]
    fn display_send_failed() {
        assert_eq!(WalError::SendFailed.to_string(), "WAL channel send failed");
    }

    #[test]
    fn display_shutdown_failed() {
        assert_eq!(WalError::ShutdownFailed.to_string(), "WAL shutdown failed");
    }

    #[test]
    fn from_io_error() {
        let io_err = std::io::Error::other("disk full");
        let wal_err: WalError = io_err.into();
        assert!(matches!(wal_err, WalError::Io(_)));
    }

    #[test]
    fn source_io() {
        use std::error::Error;
        let e = WalError::Io(std::io::Error::other("test"));
        assert!(e.source().is_some());
    }

    #[test]
    fn source_corruption_is_none() {
        use std::error::Error;
        let e = WalError::Corruption {
            message: "test".into(),
        };
        assert!(e.source().is_none());
    }

    #[test]
    fn source_closed_is_none() {
        use std::error::Error;
        assert!(WalError::Closed.source().is_none());
    }
}
