//! Synchronous, single-writer JBD2 transaction core.
//!
//! This module deliberately does not discover the journal inode.  Mount code
//! must validate the journal superblock and provide the complete logical to
//! physical block map.  Keeping mapping outside the commit path also ensures
//! that no filesystem spin lock is held while block I/O is in flight.
#![allow(dead_code)] // Activated only after every production metadata writer uses handles.

use crate::constants::BLOCK_SIZE;
use crate::ext4_defs::{Block, BlockDevice};
use crate::jbd2::{
    block_checksum, checksum_seed, commit_checksum, tag_checksum, BlockType, ChecksumMode,
    Features, Header, Superblock, BLOCK_TAIL_BYTES, CRC32C_CHKSUM, FLAG_ESCAPE, FLAG_LAST_TAG,
    FLAG_SAME_UUID, HEADER_BYTES, MAGIC,
};
use crate::prelude::*;
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};

const CLEAN: u8 = 0;
const POISONED: u8 = 1;
const MAX_COALESCED_WRITE_BLOCKS: usize = 64;

/// A fully validated journal mapping and its current allocation cursor.
///
/// `logical_blocks[n]` is the filesystem physical block containing journal
/// logical block `n`.  Entry zero is the JBD2 superblock.
pub struct JournalContext {
    pub superblock: Superblock,
    pub logical_blocks: Arc<[PBlockId]>,
    pub journal_blocks: Arc<BTreeSet<PBlockId>>,
    /// Number of addressable blocks on the filesystem device.
    pub target_blocks: u64,
    /// Next journal logical block to allocate.  It must be in the data ring.
    pub head: u32,
    /// Exact 1024-byte JBD2 superblock image read at mount.
    pub superblock_image: Box<[u8; 1024]>,
}

impl JournalContext {
    pub fn validate(&self) -> Result<()> {
        let sb = &self.superblock;
        if sb.block_size as usize != BLOCK_SIZE
            || (sb.features.checksum == ChecksumMode::V3 && sb.checksum_type != CRC32C_CHKSUM)
            || self.logical_blocks.len() != sb.max_len as usize
            || self.journal_blocks.len() < self.logical_blocks.len()
            || self.head < sb.first
            || self.head >= sb.max_len
            || self.target_blocks == 0
        {
            return Err(Ext4Error::new(ErrCode::ENOTSUP));
        }
        if self.logical_blocks.iter().any(|block| {
            *block == 0 || *block >= self.target_blocks || !self.journal_blocks.contains(block)
        }) {
            return Err(Ext4Error::new(ErrCode::EIO));
        }
        Ok(())
    }
}

/// A transaction-private metadata image.  It is intentionally not `Clone`:
/// callers cannot retain a second publishable copy past commit/abort.
pub struct StagedBlock {
    home: PBlockId,
    original: Option<Box<[u8; BLOCK_SIZE]>>,
    image: Box<[u8; BLOCK_SIZE]>,
}

impl StagedBlock {
    pub fn home(&self) -> PBlockId {
        self.home
    }

    pub fn bytes(&self) -> &[u8; BLOCK_SIZE] {
        &self.image
    }
}

/// Publishes metadata images after the selected backend has completed its
/// commit point. Journal commits publish after a durable checkpoint; direct
/// commits publish after every synchronous home-block write has succeeded.
pub trait CachePublisher: Send + Sync {
    /// Publish already-checkpointed images to in-memory caches.
    ///
    /// This callback runs after the home-block flush, so it must not allocate,
    /// perform I/O, or otherwise fail.  Borrowing the transaction's map also
    /// lets publishers inspect the final image for a particular home block
    /// without building a temporary collection.
    fn publish(&self, blocks: &BTreeMap<PBlockId, StagedBlock>);
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommitFailure {
    /// No commit record was issued; recovery will ignore any log fragments.
    BeforeCommit,
    /// Commit write or its durability flush failed, so commit is uncertain.
    CommitUncertain,
    /// The transaction committed, but home blocks are not known durable.
    CheckpointFailed,
    /// Home blocks are durable, but the journal tail was not durably cleared.
    TailUpdateFailed,
}

#[derive(Debug)]
pub struct CommitError {
    pub error: Ext4Error,
    pub failure: CommitFailure,
    /// Whether this failure poisoned the transaction core. `BeforeCommit`
    /// alone is insufficient to decide retryability: format/capacity
    /// validation leaves the core usable, while an I/O failure after the
    /// active journal tail reached disk does not.
    pub poisoned: bool,
}

/// Owns the single-writer token and poison state.  The token is held only as
/// an atomic bit; no spin guard crosses a block-device operation.
pub struct JournalTransactionCore {
    writer: AtomicBool,
    poison: AtomicU8,
    context: spin::Mutex<JournalContext>,
}

/// Synchronous nojournal transaction backend.
///
/// The writer bit is deliberately separate from the filesystem-wide metadata
/// mutation gate: the gate excludes legacy direct writers, while this token
/// owns one staged transaction within this backend.
pub struct DirectTransactionCore {
    writer: AtomicBool,
    poison: AtomicU8,
    target_blocks: u64,
}

impl DirectTransactionCore {
    pub fn new(target_blocks: u64) -> Result<Self> {
        if target_blocks == 0 {
            return Err(Ext4Error::new(ErrCode::EINVAL));
        }
        Ok(Self {
            writer: AtomicBool::new(false),
            poison: AtomicU8::new(CLEAN),
            target_blocks,
        })
    }

    pub fn is_poisoned(&self) -> bool {
        self.poison.load(Ordering::Acquire) != CLEAN
    }

    pub fn can_shutdown(&self) -> bool {
        !self.writer.load(Ordering::Acquire) && !self.is_poisoned()
    }

    pub fn start(&self, credits: usize) -> Result<Transaction<'_>> {
        self.start_with_originals(credits, false)
    }

    pub(super) fn start_direct_range(&self, credits: usize) -> Result<Transaction<'_>> {
        self.start_with_originals(credits, true)
    }

    fn start_with_originals(
        &self,
        credits: usize,
        preserve_originals: bool,
    ) -> Result<Transaction<'_>> {
        if credits == 0 || self.is_poisoned() {
            return Err(Ext4Error::new(if credits == 0 {
                ErrCode::EINVAL
            } else {
                ErrCode::EROFS
            }));
        }
        if self
            .writer
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .is_err()
        {
            return Err(Ext4Error::new(ErrCode::EAGAIN));
        }
        Ok(Transaction::new(
            TransactionCoreRef::Direct(self),
            credits,
            preserve_originals,
        ))
    }

    fn poison(&self) {
        self.poison.store(POISONED, Ordering::Release);
    }
}

impl JournalTransactionCore {
    pub fn new(context: JournalContext) -> Result<Self> {
        context.validate()?;
        Ok(Self {
            writer: AtomicBool::new(false),
            poison: AtomicU8::new(CLEAN),
            context: spin::Mutex::new(context),
        })
    }

    pub fn is_poisoned(&self) -> bool {
        self.poison.load(Ordering::Acquire) != CLEAN
    }

    pub fn can_shutdown(&self) -> bool {
        !self.writer.load(Ordering::Acquire) && !self.is_poisoned()
    }

    pub fn owns_block_range(&self, start: PBlockId, end: PBlockId) -> bool {
        start < end
            && self
                .context
                .lock()
                .journal_blocks
                .range(start..end)
                .next()
                .is_some()
    }

    pub fn credits_fit(&self, credits: usize) -> Result<bool> {
        if credits == 0 || self.is_poisoned() {
            return Err(Ext4Error::new(if credits == 0 {
                ErrCode::EINVAL
            } else {
                ErrCode::EROFS
            }));
        }
        let context = self.context.lock();
        required_log_blocks(credits, context.superblock.features)
            .and_then(|needed| ring_len(&context.superblock).map(|available| needed <= available))
    }

    pub fn start(&self, credits: usize) -> Result<Transaction<'_>> {
        if credits == 0 || self.is_poisoned() {
            return Err(Ext4Error::new(if credits == 0 {
                ErrCode::EINVAL
            } else {
                ErrCode::EROFS
            }));
        }
        if self
            .writer
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .is_err()
        {
            return Err(Ext4Error::new(ErrCode::EAGAIN));
        }

        // Reserve against a strict upper bound before any mutation is staged.
        let reservation = {
            let context = self.context.lock();
            required_log_blocks(credits, context.superblock.features).and_then(|needed| {
                ring_len(&context.superblock).map(|available| needed <= available)
            })
        };
        let fits = match reservation {
            Ok(fits) => fits,
            Err(error) => {
                self.writer.store(false, Ordering::Release);
                return Err(error);
            }
        };
        if !fits {
            self.writer.store(false, Ordering::Release);
            return Err(Ext4Error::new(ErrCode::E2BIG));
        }
        Ok(Transaction::new(
            TransactionCoreRef::Journal(self),
            credits,
            false,
        ))
    }

    fn poison(&self) {
        self.poison.store(POISONED, Ordering::Release);
    }
}

#[derive(Clone, Copy)]
enum TransactionCoreRef<'a> {
    Journal(&'a JournalTransactionCore),
    Direct(&'a DirectTransactionCore),
}

pub struct Transaction<'a> {
    core: TransactionCoreRef<'a>,
    credits: usize,
    staged: BTreeMap<PBlockId, StagedBlock>,
    preserve_originals: bool,
    owns_writer: bool,
}

impl Transaction<'_> {
    fn new(
        core: TransactionCoreRef<'_>,
        credits: usize,
        preserve_originals: bool,
    ) -> Transaction<'_> {
        Transaction {
            core,
            credits,
            staged: BTreeMap::new(),
            preserve_originals,
            owns_writer: true,
        }
    }
    /// Replace the final image for `home`.  Re-staging the same home block does
    /// not consume another credit and subsequent reads observe the replacement.
    pub fn stage(&mut self, home: PBlockId, image: Box<[u8; BLOCK_SIZE]>) -> Result<()> {
        if !self.staged.contains_key(&home) && self.staged.len() == self.credits {
            return Err(Ext4Error::new(ErrCode::E2BIG));
        }
        self.staged.insert(
            home,
            StagedBlock {
                home,
                original: None,
                image,
            },
        );
        Ok(())
    }

    /// Credits which are still available for previously untouched home blocks.
    ///
    /// Batched operations use this only to stop at a restartable boundary
    /// before beginning their next logical mutation. Re-staging an existing
    /// home remains free and the transaction is still the final authority.
    pub(super) fn remaining_credits(&self) -> usize {
        self.credits.saturating_sub(self.staged.len())
    }

    /// Return the transaction-private final image of `home` for mutation.
    ///
    /// The first access snapshots the device block and consumes one credit;
    /// later accesses return the same image, providing read-your-writes and
    /// naturally merging updates to shared metadata blocks.
    pub fn read_for_update<'a>(
        &'a mut self,
        device: &dyn BlockDevice,
        home: PBlockId,
    ) -> Result<&'a mut [u8; BLOCK_SIZE]> {
        if !self.staged.contains_key(&home) {
            if self.staged.len() == self.credits {
                return Err(Ext4Error::new(ErrCode::E2BIG));
            }
            let block = device.read_block(home)?;
            let original = self.preserve_originals.then(|| block.data.clone());
            self.staged.insert(
                home,
                StagedBlock {
                    home,
                    original,
                    image: block.data,
                },
            );
        }
        Ok(self
            .staged
            .get_mut(&home)
            .expect("staged block was just inserted")
            .image
            .as_mut())
    }

    pub fn read<'a>(&'a self, device: &dyn BlockDevice, home: PBlockId) -> Result<BlockView<'a>> {
        if let Some(block) = self.staged.get(&home) {
            Ok(BlockView::Staged(block.bytes()))
        } else {
            Ok(BlockView::Device(device.read_block(home)?))
        }
    }

    pub fn abort(mut self) {
        self.release_writer();
    }

    pub fn commit(
        mut self,
        device: &dyn BlockDevice,
        publisher: &dyn CachePublisher,
    ) -> core::result::Result<(), CommitError> {
        if self.staged.is_empty() {
            self.release_writer();
            return Ok(());
        }
        match self.core {
            TransactionCoreRef::Journal(_) => self.commit_journal(device, publisher),
            TransactionCoreRef::Direct(_) => self.commit_direct(device, publisher),
        }
    }

    /// Commit a nojournal range allocation in semantic order.
    ///
    /// Allocation homes are written before the inode-table home which makes
    /// the extent reachable.  A failure before the inode write rolls back the
    /// completed allocation homes from their transaction-private snapshots.
    /// Once the inode write is attempted its on-disk state is uncertain, so
    /// the direct backend is poisoned instead of releasing possibly-owned
    /// blocks for reuse.
    pub(super) fn commit_direct_range(
        mut self,
        device: &dyn BlockDevice,
        publisher: &dyn CachePublisher,
        allocation_homes: &[PBlockId],
        inode_home: PBlockId,
    ) -> core::result::Result<(), CommitError> {
        let TransactionCoreRef::Direct(core) = self.core else {
            return self.fail(
                Ext4Error::new(ErrCode::EINVAL),
                CommitFailure::BeforeCommit,
                false,
            );
        };
        if !device.supports_reliable_flush()
            || inode_home >= core.target_blocks
            || allocation_homes.is_empty()
            || allocation_homes.contains(&inode_home)
            || self.staged.len() != allocation_homes.len() + 1
            || !self.staged.contains_key(&inode_home)
            || allocation_homes.iter().any(|home| {
                *home >= core.target_blocks
                    || !self.staged.contains_key(home)
                    || self.staged[home].original.is_none()
            })
            || self.staged[&inode_home].original.is_none()
        {
            return self.fail(
                Ext4Error::new(ErrCode::EINVAL),
                CommitFailure::BeforeCommit,
                false,
            );
        }

        for (completed, home) in allocation_homes.iter().enumerate() {
            let staged = &self.staged[home];
            if let Err(error) = write_bytes(device, *home, staged.bytes()) {
                // The failed write may have reached the device partially.
                // Rewrite its old image as well as every earlier home before
                // declaring the operation safely aborted.
                let rollback = self.rollback_direct_range(device, &allocation_homes[..=completed]);
                return match rollback {
                    Ok(()) => self.fail(error, CommitFailure::BeforeCommit, false),
                    Err(rollback_error) => {
                        self.fail(rollback_error, CommitFailure::CheckpointFailed, true)
                    }
                };
            }
        }

        // The inode must never become durable before the allocation metadata
        // that owns its blocks. Without this boundary a volatile device cache
        // may persist the inode first and expose its blocks as free after a
        // crash. A failed flush is still before publication, so restore every
        // allocation home while the exclusive transaction owner is held.
        if let Err(error) = device.flush() {
            let rollback = self.rollback_direct_range(device, allocation_homes);
            return match rollback {
                Ok(()) => self.fail(error, CommitFailure::BeforeCommit, false),
                Err(rollback_error) => {
                    self.fail(rollback_error, CommitFailure::CheckpointFailed, true)
                }
            };
        }

        let inode = &self.staged[&inode_home];
        if let Err(error) = write_bytes(device, inode_home, inode.bytes()) {
            return self.fail(error, CommitFailure::CommitUncertain, true);
        }
        publisher.publish(&self.staged);
        self.release_writer();
        Ok(())
    }

    fn rollback_direct_range(
        &self,
        device: &dyn BlockDevice,
        completed_homes: &[PBlockId],
    ) -> Result<()> {
        for home in completed_homes.iter().rev() {
            let original = self.staged[home]
                .original
                .as_ref()
                .ok_or_else(|| Ext4Error::new(ErrCode::EIO))?;
            write_bytes(device, *home, original)?;
        }
        if !completed_homes.is_empty() && device.supports_reliable_flush() {
            device.flush()?;
        }
        Ok(())
    }

    fn commit_direct(
        mut self,
        device: &dyn BlockDevice,
        publisher: &dyn CachePublisher,
    ) -> core::result::Result<(), CommitError> {
        let TransactionCoreRef::Direct(core) = self.core else {
            unreachable!()
        };
        if self
            .staged
            .values()
            .any(|block| block.home >= core.target_blocks)
        {
            return self.fail(
                Ext4Error::new(ErrCode::EINVAL),
                CommitFailure::BeforeCommit,
                false,
            );
        }
        // BTreeMap iteration supplies a stable physical-block order without a
        // second allocation. It is deterministic, not crash atomic.
        for staged in self.staged.values() {
            if let Err(error) = write_bytes(device, staged.home, staged.bytes()) {
                return self.fail(error, CommitFailure::CheckpointFailed, true);
            }
        }
        publisher.publish(&self.staged);
        self.release_writer();
        Ok(())
    }

    fn commit_journal(
        mut self,
        device: &dyn BlockDevice,
        publisher: &dyn CachePublisher,
    ) -> core::result::Result<(), CommitError> {
        let TransactionCoreRef::Journal(core) = self.core else {
            unreachable!()
        };
        if !device.supports_reliable_flush() {
            return self.fail(
                Ext4Error::new(ErrCode::ENOTSUP),
                CommitFailure::BeforeCommit,
                false,
            );
        }

        // Copy the small allocation state, then release the spin guard before I/O.
        let (sb, mapping, journal_blocks, target_blocks, head, sb_image) = {
            let context = core.context.lock();
            (
                context.superblock,
                Arc::clone(&context.logical_blocks),
                Arc::clone(&context.journal_blocks),
                context.target_blocks,
                context.head,
                context.superblock_image.clone(),
            )
        };
        let needed =
            required_log_blocks(self.staged.len(), sb.features).map_err(|error| CommitError {
                error,
                failure: CommitFailure::BeforeCommit,
                poisoned: false,
            })?;
        if needed
            > ring_len(&sb).map_err(|error| CommitError {
                error,
                failure: CommitFailure::BeforeCommit,
                poisoned: false,
            })?
        {
            return self.fail(
                Ext4Error::new(ErrCode::E2BIG),
                CommitFailure::BeforeCommit,
                false,
            );
        }

        let sequence = sb.sequence;
        let positions = ring_positions(&sb, head, needed).map_err(|error| CommitError {
            error,
            failure: CommitFailure::BeforeCommit,
            poisoned: false,
        })?;
        if self
            .staged
            .values()
            .any(|block| block.home >= target_blocks || journal_blocks.contains(&block.home))
        {
            return self.fail(
                Ext4Error::new(ErrCode::EINVAL),
                CommitFailure::BeforeCommit,
                false,
            );
        }
        // Finish every fallible format operation before the first write.  Once
        // the active tail reaches disk, all remaining failures are I/O failures
        // which must poison the mount.
        let encoded = encode_log(&sb, sequence, &self.staged).map_err(|error| CommitError {
            error,
            failure: CommitFailure::BeforeCommit,
            poisoned: false,
        })?;
        let commit = encode_commit(&sb, sequence).map_err(|error| CommitError {
            error,
            failure: CommitFailure::BeforeCommit,
            poisoned: false,
        })?;
        let mut active_sb_image = sb_image.clone();
        update_superblock(&mut active_sb_image, sequence, head, &sb).map_err(|error| {
            CommitError {
                error,
                failure: CommitFailure::BeforeCommit,
                poisoned: false,
            }
        })?;
        let next_sequence = sequence.wrapping_add(1);
        let mut clean_sb_image = sb_image;
        update_superblock(&mut clean_sb_image, next_sequence, 0, &sb).map_err(|error| {
            CommitError {
                error,
                failure: CommitFailure::BeforeCommit,
                poisoned: false,
            }
        })?;
        let scratch_blocks = encoded
            .len()
            .max(self.staged.len())
            .min(MAX_COALESCED_WRITE_BLOCKS);
        let scratch_capacity =
            scratch_blocks
                .checked_mul(BLOCK_SIZE)
                .ok_or_else(|| CommitError {
                    error: Ext4Error::new(ErrCode::E2BIG),
                    failure: CommitFailure::BeforeCommit,
                    poisoned: false,
                })?;
        let mut write_scratch = Vec::new();
        write_scratch
            .try_reserve_exact(scratch_capacity)
            .map_err(|_| CommitError {
                error: Ext4Error::new(ErrCode::ENOMEM),
                failure: CommitFailure::BeforeCommit,
                poisoned: false,
            })?;

        // Publish an active tail before log payload.  Recovery may safely scan
        // an empty/uncommitted transaction after a crash at this point.
        if let Err(error) = write_journal_superblock(device, &mapping, &active_sb_image)
            .and_then(|_| device.flush())
        {
            return self.fail(error, CommitFailure::BeforeCommit, true);
        }

        debug_assert_eq!(encoded.len() + 1, positions.len());
        // A logical ring wrap is a recovery boundary for run construction even
        // when its physical mapping happens to be contiguous. Within each
        // logical segment, coalesce only physically contiguous journal blocks.
        let mut segment_start = 0;
        for segment_end in 1..=encoded.len() {
            let segment_complete = segment_end == encoded.len()
                || positions[segment_end - 1].checked_add(1) != Some(positions[segment_end]);
            if !segment_complete {
                continue;
            }
            let blocks = positions[segment_start..segment_end]
                .iter()
                .zip(encoded[segment_start..segment_end].iter())
                .map(|(logical, bytes)| {
                    (
                        mapping[*logical as usize],
                        bytes.as_ref() as &[u8; BLOCK_SIZE],
                    )
                });
            if let Err(error) =
                write_contiguous_blocks(device, blocks, &mut write_scratch, scratch_blocks)
            {
                return self.fail(error, CommitFailure::BeforeCommit, true);
            }
            segment_start = segment_end;
        }
        if let Err(error) = device.flush() {
            return self.fail(error, CommitFailure::BeforeCommit, true);
        }

        let commit_logical = *positions.last().unwrap();
        if let Err(error) = write_bytes(device, mapping[commit_logical as usize], &commit) {
            return self.fail(error, CommitFailure::CommitUncertain, true);
        }
        if let Err(error) = device.flush() {
            return self.fail(error, CommitFailure::CommitUncertain, true);
        }

        let home_blocks = self
            .staged
            .values()
            .map(|staged| (staged.home, staged.bytes()));
        if let Err(error) =
            write_contiguous_blocks(device, home_blocks, &mut write_scratch, scratch_blocks)
        {
            return self.fail(error, CommitFailure::CheckpointFailed, true);
        }
        if let Err(error) = device.flush() {
            return self.fail(error, CommitFailure::CheckpointFailed, true);
        }
        publisher.publish(&self.staged);

        if let Err(error) =
            write_journal_superblock(device, &mapping, &clean_sb_image).and_then(|_| device.flush())
        {
            return self.fail(error, CommitFailure::TailUpdateFailed, true);
        }
        {
            let mut context = core.context.lock();
            context.superblock.sequence = next_sequence;
            context.superblock.start = 0;
            context.head = ring_next(&sb, commit_logical);
            context.superblock_image = clean_sb_image;
        }
        self.release_writer();
        Ok(())
    }

    fn fail<T>(
        &mut self,
        error: Ext4Error,
        failure: CommitFailure,
        poison: bool,
    ) -> core::result::Result<T, CommitError> {
        if poison {
            match self.core {
                TransactionCoreRef::Journal(core) => core.poison(),
                TransactionCoreRef::Direct(core) => core.poison(),
            }
        }
        self.release_writer();
        Err(CommitError {
            error,
            failure,
            poisoned: poison,
        })
    }

    fn release_writer(&mut self) {
        if self.owns_writer {
            self.owns_writer = false;
            match self.core {
                TransactionCoreRef::Journal(core) => core.writer.store(false, Ordering::Release),
                TransactionCoreRef::Direct(core) => core.writer.store(false, Ordering::Release),
            }
        }
    }
}

impl Drop for Transaction<'_> {
    fn drop(&mut self) {
        self.release_writer();
    }
}

pub enum BlockView<'a> {
    Staged(&'a [u8; BLOCK_SIZE]),
    Device(Block),
}

impl core::ops::Deref for BlockView<'_> {
    type Target = [u8; BLOCK_SIZE];
    fn deref(&self) -> &Self::Target {
        match self {
            Self::Staged(bytes) => bytes,
            Self::Device(block) => &block.data,
        }
    }
}

fn ring_len(sb: &Superblock) -> Result<usize> {
    sb.max_len
        .checked_sub(sb.first)
        .map(|n| n as usize)
        .ok_or_else(|| Ext4Error::new(ErrCode::EIO))
}

fn tags_per_descriptor(features: Features) -> Result<usize> {
    let tail = if features.checksum == ChecksumMode::V3 {
        BLOCK_TAIL_BYTES
    } else {
        0
    };
    let overhead = HEADER_BYTES + 16 + tail;
    let available = BLOCK_SIZE
        .checked_sub(overhead)
        .ok_or_else(|| Ext4Error::new(ErrCode::EIO))?;
    let tags = available / features.tag_bytes();
    if tags == 0 {
        Err(Ext4Error::new(ErrCode::EIO))
    } else {
        Ok(tags)
    }
}

fn required_log_blocks(blocks: usize, features: Features) -> Result<usize> {
    let descriptors = blocks
        .checked_add(tags_per_descriptor(features)? - 1)
        .ok_or_else(|| Ext4Error::new(ErrCode::E2BIG))?
        / tags_per_descriptor(features)?;
    blocks
        .checked_add(descriptors)
        .and_then(|n| n.checked_add(1))
        .ok_or_else(|| Ext4Error::new(ErrCode::E2BIG))
}

fn ring_next(sb: &Superblock, current: u32) -> u32 {
    if current + 1 == sb.max_len {
        sb.first
    } else {
        current + 1
    }
}

fn ring_positions(sb: &Superblock, head: u32, count: usize) -> Result<Vec<u32>> {
    if count > ring_len(sb)? || head < sb.first || head >= sb.max_len {
        return Err(Ext4Error::new(ErrCode::E2BIG));
    }
    let mut result = Vec::with_capacity(count);
    let mut current = head;
    for _ in 0..count {
        result.push(current);
        current = ring_next(sb, current);
    }
    Ok(result)
}

fn encode_log(
    sb: &Superblock,
    sequence: u32,
    staged: &BTreeMap<PBlockId, StagedBlock>,
) -> Result<Vec<Box<[u8; BLOCK_SIZE]>>> {
    let seed = checksum_seed(&sb.uuid);
    let per_descriptor = tags_per_descriptor(sb.features)?;
    let all = staged.values().collect::<Vec<_>>();
    let mut output = Vec::new();
    for group in all.chunks(per_descriptor) {
        let mut descriptor = Box::new([0; BLOCK_SIZE]);
        Header {
            block_type: BlockType::Descriptor,
            sequence,
        }
        .encode_into(&mut descriptor[..])?;
        let mut journal_data = Vec::with_capacity(group.len());
        let mut offset = HEADER_BYTES;
        for (index, staged) in group.iter().enumerate() {
            let mut image = staged.image.clone();
            let mut flags = if index != 0 { FLAG_SAME_UUID } else { 0 };
            if u32::from_be_bytes(
                image[..4]
                    .try_into()
                    .map_err(|_| Ext4Error::new(ErrCode::EIO))?,
            ) == MAGIC
            {
                image[..4].fill(0);
                flags |= FLAG_ESCAPE;
            }
            if index + 1 == group.len() {
                flags |= FLAG_LAST_TAG;
            }
            descriptor[offset..offset + 4].copy_from_slice(&(staged.home as u32).to_be_bytes());
            match sb.features.checksum {
                ChecksumMode::V3 => {
                    if !sb.features.has_64bit && staged.home > u32::MAX as u64 {
                        return Err(Ext4Error::new(ErrCode::ENOTSUP));
                    }
                    descriptor[offset + 4..offset + 8].copy_from_slice(&flags.to_be_bytes());
                    descriptor[offset + 8..offset + 12]
                        .copy_from_slice(&((staged.home >> 32) as u32).to_be_bytes());
                    descriptor[offset + 12..offset + 16]
                        .copy_from_slice(&tag_checksum(seed, sequence, &image[..]).to_be_bytes());
                }
                ChecksumMode::None => {
                    if staged.home > u32::MAX as u64 {
                        return Err(Ext4Error::new(ErrCode::ENOTSUP));
                    }
                    descriptor[offset + 6..offset + 8]
                        .copy_from_slice(&(flags as u16).to_be_bytes());
                }
            }
            offset += sb.features.tag_bytes();
            if index == 0 {
                descriptor[offset..offset + 16].copy_from_slice(&sb.uuid);
                offset += 16;
            }
            journal_data.push(image);
        }
        if sb.features.checksum == ChecksumMode::V3 {
            let checksum = block_checksum(seed, &descriptor[..])?;
            descriptor[BLOCK_SIZE - 4..].copy_from_slice(&checksum.to_be_bytes());
        }
        output.push(descriptor);
        output.extend(journal_data);
    }
    Ok(output)
}

fn encode_commit(sb: &Superblock, sequence: u32) -> Result<Box<[u8; BLOCK_SIZE]>> {
    let mut block = Box::new([0; BLOCK_SIZE]);
    Header {
        block_type: BlockType::Commit,
        sequence,
    }
    .encode_into(&mut block[..])?;
    if sb.features.checksum == ChecksumMode::V3 {
        let checksum = commit_checksum(checksum_seed(&sb.uuid), &block[..])?;
        block[16..20].copy_from_slice(&checksum.to_be_bytes());
    }
    Ok(block)
}

fn update_superblock(
    image: &mut [u8; 1024],
    sequence: u32,
    start: u32,
    sb: &Superblock,
) -> Result<()> {
    image[24..28].copy_from_slice(&sequence.to_be_bytes());
    image[28..32].copy_from_slice(&start.to_be_bytes());
    if sb.features.checksum == ChecksumMode::V3 {
        image[252..256].fill(0);
        let checksum = crate::jbd2::superblock_checksum(image)?;
        image[252..256].copy_from_slice(&checksum.to_be_bytes());
    }
    Ok(())
}

fn write_journal_superblock(
    device: &dyn BlockDevice,
    mapping: &[PBlockId],
    image: &[u8; 1024],
) -> Result<()> {
    let mut block = device.read_block(mapping[0])?;
    block.data[..1024].copy_from_slice(image);
    device.write_block(&block)
}

fn write_bytes(device: &dyn BlockDevice, id: PBlockId, bytes: &[u8; BLOCK_SIZE]) -> Result<()> {
    device.write_block(&Block::new(id, Box::new(*bytes)))
}

fn write_contiguous_blocks<'a>(
    device: &dyn BlockDevice,
    blocks: impl IntoIterator<Item = (PBlockId, &'a [u8; BLOCK_SIZE])>,
    scratch: &mut Vec<u8>,
    max_blocks: usize,
) -> Result<()> {
    debug_assert!(
        max_blocks <= MAX_COALESCED_WRITE_BLOCKS && scratch.capacity() >= max_blocks * BLOCK_SIZE,
        "journal write scratch must be reserved before the active tail"
    );
    scratch.clear();
    let mut first: Option<PBlockId> = None;
    let mut previous: Option<PBlockId> = None;

    for (physical, image) in blocks {
        let at_capacity = scratch.len() == max_blocks * BLOCK_SIZE;
        let contiguous = previous.and_then(|previous| previous.checked_add(1)) == Some(physical);
        if !scratch.is_empty() && (at_capacity || !contiguous) {
            device.write_blocks(first.expect("non-empty run has a first block"), scratch)?;
            scratch.clear();
            first = None;
        }
        if first.is_none() {
            first = Some(physical);
        }
        debug_assert!(scratch.len() + BLOCK_SIZE <= scratch.capacity());
        scratch.extend_from_slice(image);
        previous = Some(physical);
    }

    if !scratch.is_empty() {
        device.write_blocks(first.expect("non-empty run has a first block"), scratch)?;
        scratch.clear();
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use core::sync::atomic::{AtomicUsize, Ordering};

    const TEST_MAX_FAST_METADATA_IMAGES: usize = 16;
    const TEST_LARGE_JOURNAL_BLOCKS: u32 = 64;

    struct MemoryDevice {
        volatile: spin::Mutex<BTreeMap<PBlockId, Box<[u8; BLOCK_SIZE]>>>,
        stable: spin::Mutex<BTreeMap<PBlockId, Box<[u8; BLOCK_SIZE]>>>,
        operation: AtomicUsize,
        fail_at: AtomicUsize,
        fail_at_second: AtomicUsize,
        write_order: spin::Mutex<Vec<PBlockId>>,
        bulk_writes: spin::Mutex<Vec<(PBlockId, usize)>>,
        bulk_calls: AtomicUsize,
        partial_bulk_call: AtomicUsize,
        partial_bulk_prefix: AtomicUsize,
        reads: AtomicUsize,
        writes: AtomicUsize,
        flushes: AtomicUsize,
    }

    impl MemoryDevice {
        fn new() -> Self {
            Self {
                volatile: spin::Mutex::new(BTreeMap::new()),
                stable: spin::Mutex::new(BTreeMap::new()),
                operation: AtomicUsize::new(0),
                fail_at: AtomicUsize::new(usize::MAX),
                fail_at_second: AtomicUsize::new(usize::MAX),
                write_order: spin::Mutex::new(Vec::new()),
                bulk_writes: spin::Mutex::new(Vec::new()),
                bulk_calls: AtomicUsize::new(0),
                partial_bulk_call: AtomicUsize::new(usize::MAX),
                partial_bulk_prefix: AtomicUsize::new(usize::MAX),
                reads: AtomicUsize::new(0),
                writes: AtomicUsize::new(0),
                flushes: AtomicUsize::new(0),
            }
        }

        fn step(&self) -> Result<()> {
            let operation = self.operation.fetch_add(1, Ordering::SeqCst);
            if operation == self.fail_at.load(Ordering::SeqCst)
                || operation == self.fail_at_second.load(Ordering::SeqCst)
            {
                Err(Ext4Error::new(ErrCode::EIO))
            } else {
                Ok(())
            }
        }

        /// Simulate power loss: only the last completed flush epoch survives.
        fn crash(&self) {
            *self.volatile.lock() = self.stable.lock().clone();
        }

        fn stable_block(&self, id: PBlockId) -> Option<Box<[u8; BLOCK_SIZE]>> {
            self.stable.lock().get(&id).cloned()
        }
    }

    impl BlockDevice for MemoryDevice {
        fn read_block(&self, block_id: PBlockId) -> Result<Block> {
            self.reads.fetch_add(1, Ordering::SeqCst);
            Ok(Block::new(
                block_id,
                self.volatile
                    .lock()
                    .get(&block_id)
                    .cloned()
                    .unwrap_or_else(|| Box::new([0; BLOCK_SIZE])),
            ))
        }

        fn write_block(&self, block: &Block) -> Result<()> {
            self.writes.fetch_add(1, Ordering::SeqCst);
            self.step()?;
            self.write_order.lock().push(block.id);
            self.volatile.lock().insert(block.id, block.data.clone());
            Ok(())
        }

        fn write_blocks(&self, start: PBlockId, data: &[u8]) -> Result<()> {
            if data.is_empty() || !data.len().is_multiple_of(BLOCK_SIZE) {
                return Err(Ext4Error::new(ErrCode::EINVAL));
            }
            let blocks = data.len() / BLOCK_SIZE;
            let call = self.bulk_calls.fetch_add(1, Ordering::SeqCst);
            self.bulk_writes.lock().push((start, blocks));
            let partial = call == self.partial_bulk_call.load(Ordering::SeqCst);
            let prefix = self.partial_bulk_prefix.load(Ordering::SeqCst);

            for (index, bytes) in data.chunks_exact(BLOCK_SIZE).enumerate() {
                if partial && index == prefix {
                    return Err(Ext4Error::new(ErrCode::EIO));
                }
                let id = start
                    .checked_add(index as PBlockId)
                    .ok_or_else(|| Ext4Error::new(ErrCode::EFBIG))?;
                let mut image = Box::new([0; BLOCK_SIZE]);
                image.copy_from_slice(bytes);
                self.write_block(&Block::new(id, image))?;
                if partial {
                    // A failed multi-block request does not promise that its
                    // completed prefix remained only in volatile cache.
                    // Persist the prefix to model the conservative recovery
                    // case required from a real block device after EIO.
                    let stable_image = self.volatile.lock()[&id].clone();
                    self.stable.lock().insert(id, stable_image);
                }
            }
            if partial && prefix >= blocks {
                return Err(Ext4Error::new(ErrCode::EIO));
            }
            Ok(())
        }

        fn flush(&self) -> Result<()> {
            self.flushes.fetch_add(1, Ordering::SeqCst);
            self.step()?;
            *self.stable.lock() = self.volatile.lock().clone();
            Ok(())
        }

        fn supports_reliable_flush(&self) -> bool {
            true
        }
    }

    struct Publisher(AtomicUsize);
    impl CachePublisher for Publisher {
        fn publish(&self, blocks: &BTreeMap<PBlockId, StagedBlock>) {
            self.0.fetch_add(blocks.len(), Ordering::SeqCst);
        }
    }

    #[test]
    fn direct_commit_writes_home_blocks_in_order_then_publishes() {
        let device = MemoryDevice::new();
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let mut transaction = core.start(3).unwrap();
        transaction.stage(9, Box::new([9; BLOCK_SIZE])).unwrap();
        transaction.stage(2, Box::new([2; BLOCK_SIZE])).unwrap();
        transaction.stage(5, Box::new([5; BLOCK_SIZE])).unwrap();

        transaction.commit(&device, &publisher).unwrap();

        assert_eq!(&*device.write_order.lock(), &[2, 5, 9]);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 3);
        assert!(!core.is_poisoned());
        assert!(core.can_shutdown());
    }

    #[test]
    fn direct_write_failure_never_publishes_and_poisons_backend() {
        let device = MemoryDevice::new();
        device.fail_at.store(1, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let mut transaction = core.start(2).unwrap();
        transaction.stage(2, Box::new([2; BLOCK_SIZE])).unwrap();
        transaction.stage(5, Box::new([5; BLOCK_SIZE])).unwrap();

        let error = transaction.commit(&device, &publisher).unwrap_err();

        assert_eq!(error.failure, CommitFailure::CheckpointFailed);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        assert!(core.is_poisoned());
        assert_eq!(core.start(1).err().unwrap().code(), ErrCode::EROFS);
    }

    #[test]
    fn direct_validation_failure_releases_writer_without_poison() {
        let device = MemoryDevice::new();
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(8).unwrap();
        let mut transaction = core.start(1).unwrap();
        transaction.stage(8, Box::new([1; BLOCK_SIZE])).unwrap();

        let error = transaction.commit(&device, &publisher).unwrap_err();

        assert_eq!(error.failure, CommitFailure::BeforeCommit);
        assert!(!core.is_poisoned());
        assert!(core.start(1).is_ok());
    }

    #[test]
    fn direct_backend_allows_only_one_transaction_owner() {
        let core = DirectTransactionCore::new(8).unwrap();
        let transaction = core.start(1).unwrap();
        assert_eq!(core.start(1).err().unwrap().code(), ErrCode::EAGAIN);
        transaction.abort();
        assert!(core.start(1).is_ok());
    }

    #[test]
    fn ordinary_transactions_do_not_retain_rollback_images() {
        let device = MemoryDevice::new();
        let core = DirectTransactionCore::new(8).unwrap();
        let mut transaction = core.start(1).unwrap();

        transaction.read_for_update(&device, 1).unwrap()[0] = 1;

        assert!(transaction.staged[&1].original.is_none());
        transaction.abort();
    }

    #[test]
    fn direct_range_transactions_retain_rollback_images() {
        let device = MemoryDevice::new();
        let core = DirectTransactionCore::new(8).unwrap();
        let mut transaction = core.start_direct_range(1).unwrap();

        transaction.read_for_update(&device, 1).unwrap()[0] = 1;

        assert!(transaction.staged[&1].original.is_some());
        transaction.abort();
    }

    fn staged_direct_range<'a>(
        core: &'a DirectTransactionCore,
        device: &MemoryDevice,
    ) -> Transaction<'a> {
        let mut transaction = core.start_direct_range(4).unwrap();
        for (home, value) in [(9, 9), (2, 2), (5, 5), (7, 7)] {
            transaction
                .read_for_update(device, home)
                .unwrap()
                .fill(value);
        }
        transaction
    }

    #[test]
    fn direct_range_uses_allocation_order_and_inode_last() {
        let device = MemoryDevice::new();
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap();

        assert_eq!(&*device.write_order.lock(), &[9, 2, 5, 7]);
        assert_eq!(device.flushes.load(Ordering::SeqCst), 1);
        assert_eq!(device.stable_block(9).unwrap().as_slice(), &[9; BLOCK_SIZE]);
        assert!(device.stable_block(7).is_none());
        assert_eq!(publisher.0.load(Ordering::SeqCst), 4);
        assert!(!core.is_poisoned());
    }

    #[test]
    fn direct_range_restores_failed_allocation_home_before_continuing() {
        let device = MemoryDevice::new();
        device.fail_at.store(1, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        let error = transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::BeforeCommit);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        assert_eq!(&*device.write_order.lock(), &[9, 2, 9]);
        assert_eq!(device.volatile.lock()[&9].as_slice(), &[0; BLOCK_SIZE]);
        assert_eq!(device.volatile.lock()[&2].as_slice(), &[0; BLOCK_SIZE]);
        assert!(!core.is_poisoned());
    }

    #[test]
    fn direct_range_restores_each_failed_allocation_home() {
        for failed_home in 0..3 {
            let device = MemoryDevice::new();
            device.fail_at.store(failed_home, Ordering::SeqCst);
            let publisher = Publisher(AtomicUsize::new(0));
            let core = DirectTransactionCore::new(128).unwrap();
            let transaction = staged_direct_range(&core, &device);

            let error = transaction
                .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
                .unwrap_err();

            assert_eq!(error.failure, CommitFailure::BeforeCommit);
            assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
            for home in [9, 2, 5] {
                let image = device
                    .volatile
                    .lock()
                    .get(&home)
                    .cloned()
                    .unwrap_or_else(|| Box::new([0; BLOCK_SIZE]));
                assert_eq!(image.as_slice(), &[0; BLOCK_SIZE]);
            }
            assert!(!core.is_poisoned());
        }
    }

    #[test]
    fn direct_range_rollback_write_failure_poisons_backend() {
        let device = MemoryDevice::new();
        device.fail_at.store(1, Ordering::SeqCst);
        device.fail_at_second.store(2, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        let error = transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::CheckpointFailed);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        assert!(core.is_poisoned());
    }

    #[test]
    fn direct_range_rollback_flush_failure_poisons_backend() {
        let device = MemoryDevice::new();
        device.fail_at.store(1, Ordering::SeqCst);
        device.fail_at_second.store(4, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        let error = transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::CheckpointFailed);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        assert!(core.is_poisoned());
    }

    #[test]
    fn direct_range_inode_write_failure_is_uncertain_and_poisons() {
        let device = MemoryDevice::new();
        device.fail_at.store(4, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        let error = transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::CommitUncertain);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        assert!(core.is_poisoned());
    }

    #[test]
    fn direct_range_allocation_flush_failure_restores_all_homes() {
        let device = MemoryDevice::new();
        device.fail_at.store(3, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        let error = transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::BeforeCommit);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        for home in [9, 2, 5] {
            assert_eq!(
                device.stable_block(home).unwrap().as_slice(),
                &[0; BLOCK_SIZE]
            );
        }
        assert!(device.stable_block(7).is_none());
        assert!(!core.is_poisoned());
    }

    #[test]
    fn direct_range_allocation_flush_rollback_failure_poisons_backend() {
        let device = MemoryDevice::new();
        device.fail_at.store(3, Ordering::SeqCst);
        device.fail_at_second.store(4, Ordering::SeqCst);
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(128).unwrap();
        let transaction = staged_direct_range(&core, &device);

        let error = transaction
            .commit_direct_range(&device, &publisher, &[9, 2, 5], 7)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::CheckpointFailed);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        assert!(core.is_poisoned());
    }

    #[test]
    fn direct_range_rejects_out_of_range_inode_without_io() {
        let device = MemoryDevice::new();
        let publisher = Publisher(AtomicUsize::new(0));
        let core = DirectTransactionCore::new(8).unwrap();
        let mut transaction = core.start_direct_range(4).unwrap();
        for home in [1, 2, 3, 8] {
            transaction.read_for_update(&device, home).unwrap()[0] = 1;
        }

        let error = transaction
            .commit_direct_range(&device, &publisher, &[1, 2, 3], 8)
            .unwrap_err();

        assert_eq!(error.failure, CommitFailure::BeforeCommit);
        assert_eq!(device.writes.load(Ordering::SeqCst), 0);
        assert!(!core.is_poisoned());
    }

    #[test]
    fn read_for_update_merges_shared_block_and_charges_one_credit() {
        let device = MemoryDevice::new();
        let core = JournalTransactionCore::new(context()).unwrap();
        let mut transaction = core.start(1).unwrap();

        transaction.read_for_update(&device, 42).unwrap()[10] = 1;
        transaction.read_for_update(&device, 42).unwrap()[11] = 2;
        assert_eq!(transaction.read(&device, 42).unwrap()[10..12], [1, 2]);
        assert_eq!(
            transaction.read_for_update(&device, 43).unwrap_err().code(),
            ErrCode::E2BIG
        );
    }

    fn context() -> JournalContext {
        context_with_ring(8, 7)
    }

    fn context_with_ring(max_len: u32, head: u32) -> JournalContext {
        let features = Features::validate(
            0,
            crate::jbd2::FEATURE_INCOMPAT_CSUM_V3 | crate::jbd2::FEATURE_INCOMPAT_64BIT,
            0,
        )
        .unwrap();
        let mut image = Box::new([0; 1024]);
        Header {
            block_type: BlockType::SuperblockV2,
            sequence: 7,
        }
        .encode_into(&mut image[..])
        .unwrap();
        image[12..16].copy_from_slice(&(BLOCK_SIZE as u32).to_be_bytes());
        image[16..20].copy_from_slice(&max_len.to_be_bytes());
        image[20..24].copy_from_slice(&1u32.to_be_bytes());
        image[24..28].copy_from_slice(&head.to_be_bytes());
        image[40..44].copy_from_slice(
            &(crate::jbd2::FEATURE_INCOMPAT_CSUM_V3 | crate::jbd2::FEATURE_INCOMPAT_64BIT)
                .to_be_bytes(),
        );
        image[48..64].copy_from_slice(b"0123456789abcdef");
        image[80] = CRC32C_CHKSUM;
        update_superblock(
            &mut image,
            head,
            0,
            &Superblock {
                block_size: BLOCK_SIZE as u32,
                max_len,
                first: 1,
                sequence: 7,
                start: 0,
                errno: 0,
                features,
                uuid: *b"0123456789abcdef",
                checksum_type: CRC32C_CHKSUM,
            },
        )
        .unwrap();
        JournalContext {
            superblock: Superblock::parse(&image[..], BLOCK_SIZE as u32).unwrap(),
            logical_blocks: Vec::from_iter(100..100 + max_len as u64).into(),
            journal_blocks: Arc::new(BTreeSet::from_iter(100..100 + max_len as u64)),
            target_blocks: 1000,
            head,
            superblock_image: image,
        }
    }

    fn context_with_fragmented_log() -> JournalContext {
        let mut context = context_with_ring(TEST_LARGE_JOURNAL_BLOCKS, 1);
        let mut mapping = context.logical_blocks.to_vec();
        for (logical, physical) in mapping.iter_mut().enumerate().skip(1) {
            *physical = 200 + logical as PBlockId * 2;
        }
        context.journal_blocks = Arc::new(mapping.iter().copied().collect());
        context.logical_blocks = mapping.into();
        context
    }

    fn assert_counted_commit_shape(
        homes: usize,
        expected_reads: usize,
        expected_writes: usize,
        expected_bulk_writes: &[(PBlockId, usize)],
    ) {
        let device = MemoryDevice::new();
        let publisher = Publisher(AtomicUsize::new(0));
        let core = JournalTransactionCore::new(context_with_ring(
            TEST_LARGE_JOURNAL_BLOCKS,
            TEST_LARGE_JOURNAL_BLOCKS - 1,
        ))
        .unwrap();
        let mut transaction = core.start(homes).unwrap();
        for home in 42..42 + homes as u64 {
            transaction.read_for_update(&device, home).unwrap()[0] = home as u8;
        }

        transaction.commit(&device, &publisher).unwrap();

        assert_eq!(device.reads.load(Ordering::SeqCst), expected_reads);
        assert_eq!(device.writes.load(Ordering::SeqCst), expected_writes);
        assert_eq!(device.flushes.load(Ordering::SeqCst), 5);
        assert_eq!(&*device.bulk_writes.lock(), expected_bulk_writes);
        assert_eq!(publisher.0.load(Ordering::SeqCst), homes);
        assert!(!core.is_poisoned());
    }

    #[test]
    fn journal_commit_four_homes_has_expected_fixed_io_shape() {
        assert_counted_commit_shape(4, 6, 12, &[(163, 1), (101, 4), (42, 4)]);
    }

    #[test]
    fn journal_commit_max_fast_metadata_images_has_expected_fixed_io_shape() {
        assert_counted_commit_shape(
            TEST_MAX_FAST_METADATA_IMAGES,
            18,
            36,
            &[(163, 1), (101, 16), (42, 16)],
        );
    }

    #[test]
    fn journal_commit_splits_physically_fragmented_log_runs() {
        let device = MemoryDevice::new();
        let publisher = Publisher(AtomicUsize::new(0));
        let core = JournalTransactionCore::new(context_with_fragmented_log()).unwrap();

        staged_journal_transaction(&core, 4)
            .commit(&device, &publisher)
            .unwrap();

        assert_eq!(
            &*device.bulk_writes.lock(),
            &[(202, 1), (204, 1), (206, 1), (208, 1), (210, 1), (42, 4)]
        );
        assert_eq!(device.flushes.load(Ordering::SeqCst), 5);
        assert_eq!(publisher.0.load(Ordering::SeqCst), 4);
    }

    #[test]
    fn journal_core_rejects_out_of_range_mapping_at_construction() {
        let mut context = context_with_ring(TEST_LARGE_JOURNAL_BLOCKS, 1);
        let mut mapping = context.logical_blocks.to_vec();
        mapping[2] = context.target_blocks;
        context.journal_blocks = Arc::new(mapping.iter().copied().collect());
        context.logical_blocks = mapping.into();

        let error = JournalTransactionCore::new(context).err().unwrap();
        assert_eq!(error.code(), ErrCode::EIO);
    }

    fn staged_journal_transaction<'a>(
        core: &'a JournalTransactionCore,
        homes: usize,
    ) -> Transaction<'a> {
        let mut transaction = core.start(homes).unwrap();
        for home in 42..42 + homes as PBlockId {
            transaction
                .stage(home, Box::new([home as u8; BLOCK_SIZE]))
                .unwrap();
        }
        transaction
    }

    #[test]
    fn journal_bulk_log_partial_failure_never_publishes() {
        for prefix in [0, 1, 2, 4] {
            let device = MemoryDevice::new();
            device.partial_bulk_call.store(0, Ordering::SeqCst);
            device.partial_bulk_prefix.store(prefix, Ordering::SeqCst);
            let core = JournalTransactionCore::new(context_with_ring(TEST_LARGE_JOURNAL_BLOCKS, 1))
                .unwrap();
            let publisher = Publisher(AtomicUsize::new(0));

            let error = staged_journal_transaction(&core, 4)
                .commit(&device, &publisher)
                .unwrap_err();

            assert_eq!(error.failure, CommitFailure::BeforeCommit);
            assert!(core.is_poisoned());
            assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
            assert_eq!(device.stable_block(101).is_some(), prefix != 0);
            assert!(device.stable_block(42).is_none());
            device.crash();
            assert!(device.stable_block(42).is_none());
        }
    }

    #[test]
    fn journal_bulk_home_partial_failure_keeps_committed_log_for_recovery() {
        for prefix in [0, 1, 2, 3, 4] {
            let device = MemoryDevice::new();
            device.partial_bulk_call.store(1, Ordering::SeqCst);
            device.partial_bulk_prefix.store(prefix, Ordering::SeqCst);
            let core = JournalTransactionCore::new(context_with_ring(TEST_LARGE_JOURNAL_BLOCKS, 1))
                .unwrap();
            let publisher = Publisher(AtomicUsize::new(0));

            let error = staged_journal_transaction(&core, 4)
                .commit(&device, &publisher)
                .unwrap_err();

            assert_eq!(error.failure, CommitFailure::CheckpointFailed);
            assert!(core.is_poisoned());
            assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
            for index in 0..4 {
                assert_eq!(
                    device.stable_block(42 + index as PBlockId).is_some(),
                    index < prefix
                );
            }
            // The commit block is stable before checkpoint starts, so mount
            // recovery has the authoritative complete transaction even when
            // an arbitrary home-block prefix reached stable media.
            assert!(device.stable_block(106).is_some());
            device.crash();
            assert!(device.stable_block(106).is_some());
        }
    }

    #[test]
    fn encodes_escape_and_wraps_ring_without_losing_home_image() {
        let device = MemoryDevice::new();
        let core = JournalTransactionCore::new(context()).unwrap();
        let publisher = Publisher(AtomicUsize::new(0));
        let mut transaction = core.start(1).unwrap();
        let mut image = Box::new([0x5a; BLOCK_SIZE]);
        image[..4].copy_from_slice(&MAGIC.to_be_bytes());
        transaction.stage(42, image).unwrap();
        transaction.commit(&device, &publisher).unwrap();

        // head=7: descriptor at 7, escaped data wraps to 1, commit at 2.
        assert_eq!(&device.stable_block(101).unwrap()[..4], &[0, 0, 0, 0]);
        assert_eq!(&device.stable_block(42).unwrap()[..4], &MAGIC.to_be_bytes());
        assert_eq!(publisher.0.load(Ordering::SeqCst), 1);
        assert!(!core.is_poisoned());
    }

    #[test]
    fn commit_flush_failure_never_publishes_or_checkpoints() {
        let device = MemoryDevice::new();
        // active sb write+flush, descriptor+data writes+flush, commit write,
        // then fail the commit-point flush (zero-based operation 6).
        device.fail_at.store(6, Ordering::SeqCst);
        let core = JournalTransactionCore::new(context()).unwrap();
        let publisher = Publisher(AtomicUsize::new(0));
        let mut transaction = core.start(1).unwrap();
        transaction.stage(42, Box::new([0x33; BLOCK_SIZE])).unwrap();
        let error = transaction.commit(&device, &publisher).unwrap_err();
        assert_eq!(error.failure, CommitFailure::CommitUncertain);
        assert!(core.is_poisoned());
        assert_eq!(publisher.0.load(Ordering::SeqCst), 0);
        device.crash();
        assert!(device.stable_block(42).is_none());
    }

    #[test]
    fn staging_is_deduplicated_and_reads_its_latest_write() {
        let device = MemoryDevice::new();
        let core = JournalTransactionCore::new(context()).unwrap();
        let mut transaction = core.start(1).unwrap();
        transaction.stage(42, Box::new([1; BLOCK_SIZE])).unwrap();
        transaction.stage(42, Box::new([2; BLOCK_SIZE])).unwrap();
        assert_eq!(transaction.read(&device, 42).unwrap()[0], 2);
        transaction.abort();
        assert!(core.start(1).is_ok());
    }
}
