//! M0 exit criteria validation tests.
//!
//! These integration tests validate the storage layer's crash safety, WAL
//! replay, free-list reclamation, and FsDirectory equivalence — the exit
//! criteria that must pass before M1 begins.
//!
//! See [[architecture-overview#Exit Criteria]].

#[cfg(unix)]
mod crash_safety {
    use std::fs;
    use std::path::PathBuf;

    use luci_core::SegmentId;
    use luci_storage::SingleFileDirectory;

    fn test_dir() -> PathBuf {
        let dir = std::env::temp_dir()
            .join(format!("luci_exit_criteria_{}", std::process::id()));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    /// Crash fuzz test: fork a child that writes and commits in a loop, kill
    /// it with SIGKILL at random points, verify the parent can open the file
    /// and read the last successful commit.
    ///
    /// Validates the sector-atomic 4 KB header write assumption and the
    /// two-root-pointer crash recovery protocol.
    #[test]
    fn crash_fuzz_sigkill() {
        use rand::Rng;

        // Default to 100 iterations; set LUCI_CRASH_FUZZ_ITERATIONS for more.
        let iterations: u32 = std::env::var("LUCI_CRASH_FUZZ_ITERATIONS")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(100);

        let mut rng = rand::thread_rng();

        for i in 0..iterations {
            let path = test_dir().join(format!("crash_fuzz_{i}.luci"));
            let _ = fs::remove_file(&path);

            // Phase 1: Create file with committed data the child can build on.
            let initial_segments: u64 = rng.gen_range(1..=5);
            let mut expected_data: Vec<(SegmentId, Vec<u8>)> = Vec::new();
            {
                let mut dir = SingleFileDirectory::create(&path).unwrap();
                for j in 0..initial_segments {
                    let size = 100 + (j as usize * 73 + i as usize * 37) % 5000;
                    let data: Vec<u8> = (0..size).map(|k| (k as u8).wrapping_mul(0x37)).collect();
                    let seg_id = SegmentId::new(j);
                    dir.write_segment(seg_id, &data).unwrap();
                    expected_data.push((seg_id, data));
                }
                dir.commit().unwrap();
            }

            // Phase 2: Fork child to write/commit in a tight loop.
            unsafe {
                let pid = libc::fork();
                if pid == 0 {
                    // Child process: open file and write/commit indefinitely.
                    // If open fails (e.g., previous iteration left bad state),
                    // exit cleanly — the parent will still validate.
                    let Ok(mut dir) = SingleFileDirectory::open(&path) else {
                        libc::_exit(1);
                    };
                    let mut seg_id = 1000u64;
                    loop {
                        let size = 500 + (seg_id as usize % 10000);
                        let data = vec![0xABu8; size];
                        if dir.write_segment(SegmentId::new(seg_id), &data).is_err() {
                            libc::_exit(1);
                        }
                        if dir.commit().is_err() {
                            libc::_exit(1);
                        }
                        seg_id += 1;
                    }
                } else if pid > 0 {
                    // Parent: wait a random duration, then SIGKILL the child.
                    let sleep_us = rng.gen_range(50..5000);
                    libc::usleep(sleep_us);
                    libc::kill(pid, libc::SIGKILL);
                    let mut status: libc::c_int = 0;
                    libc::waitpid(pid, &mut status, 0);
                } else {
                    panic!("fork failed on iteration {i}");
                }
            }

            // Phase 3: Verify the file is recoverable.
            let dir = SingleFileDirectory::open(&path)
                .unwrap_or_else(|e| panic!("iteration {i}: failed to open after crash: {e}"));

            // Must have at least the initial committed segments.
            assert!(
                dir.segments().len() >= expected_data.len(),
                "iteration {i}: expected at least {} segments, got {}",
                expected_data.len(),
                dir.segments().len()
            );

            // Original committed data must be intact.
            for (seg_id, expected) in &expected_data {
                let actual = dir.read_segment(*seg_id).unwrap_or_else(|e| {
                    panic!("iteration {i}: failed to read segment {seg_id}: {e}")
                });
                assert_eq!(
                    &actual, expected,
                    "iteration {i}: segment {seg_id} data mismatch"
                );
            }

            // All listed segments must be readable (no corrupted entries).
            for entry in dir.segments() {
                let data = dir.read_segment(entry.segment_id).unwrap_or_else(|e| {
                    panic!(
                        "iteration {i}: listed segment {} unreadable: {e}",
                        entry.segment_id
                    )
                });
                assert_eq!(
                    data.len() as u64, entry.data_len,
                    "iteration {i}: segment {} data_len mismatch",
                    entry.segment_id
                );
            }

            let _ = fs::remove_file(&path);
        }
    }

    /// Verify that uncommitted writes at various stages of the commit protocol
    /// don't corrupt the file. Tests deterministic crash points rather than
    /// random timing.
    #[test]
    fn uncommitted_writes_never_corrupt() {
        let path = test_dir().join("uncommitted_never_corrupt.luci");
        let _ = fs::remove_file(&path);

        for round in 0..50 {
            let _ = fs::remove_file(&path);

            let committed_data = vec![0xCDu8; 1000 + round * 100];
            {
                let mut dir = SingleFileDirectory::create(&path).unwrap();
                dir.write_segment(SegmentId::new(1), &committed_data)
                    .unwrap();
                dir.commit().unwrap();

                // Write but don't commit — simulates crash before commit.
                let uncommitted = vec![0xEFu8; 2000 + round * 50];
                dir.write_segment(SegmentId::new(2), &uncommitted).unwrap();
                // Drop without commit.
            }

            // File should recover to the committed state.
            let dir = SingleFileDirectory::open(&path).unwrap();
            assert_eq!(dir.segments().len(), 1);
            assert_eq!(
                dir.read_segment(SegmentId::new(1)).unwrap(),
                committed_data
            );
            assert!(dir.read_segment(SegmentId::new(2)).is_err());
        }

        let _ = fs::remove_file(&path);
    }
}

#[cfg(unix)]
mod wal_replay {
    use std::fs;
    use std::path::PathBuf;

    use luci_core::{DocId, SegmentId};
    use luci_storage::{
        replay_wal, DurabilityMode, SingleFileDirectory, Wal, WalRecord,
    };

    fn test_dir() -> PathBuf {
        let dir = std::env::temp_dir()
            .join(format!("luci_exit_wal_{}", std::process::id()));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    /// Integration test: WAL replay reconstructs uncommitted data after crash.
    ///
    /// Simulates the full crash recovery flow:
    /// 1. Write committed segments to SingleFileDirectory
    /// 2. Write document operations to WAL (uncommitted)
    /// 3. Simulate crash (drop both)
    /// 4. Reopen: load committed state + replay WAL
    /// 5. Verify both committed and uncommitted data are available
    #[test]
    fn wal_replay_reconstructs_uncommitted_data() {
        let dir_path = test_dir().join("wal_replay.luci");
        let wal_path = test_dir().join("wal_replay.luci.wal");
        let _ = fs::remove_file(&dir_path);
        let _ = fs::remove_file(&wal_path);

        let committed_segment_data = b"committed segment bytes";

        // Phase 1: Write committed data + uncommitted WAL records.
        {
            let mut dir = SingleFileDirectory::create(&dir_path).unwrap();
            dir.write_segment(SegmentId::new(1), committed_segment_data)
                .unwrap();
            dir.commit().unwrap();

            // Write document operations to WAL without committing a new segment.
            let mut wal = Wal::open(&wal_path, DurabilityMode::Full).unwrap();
            for i in 0..100 {
                wal.append(&WalRecord::Put {
                    doc_id: DocId::new(i),
                    data: format!("{{\"title\": \"doc-{i}\"}}").into_bytes(),
                })
                .unwrap();
            }
            wal.append(&WalRecord::Delete {
                doc_id: DocId::new(50),
            })
            .unwrap();
            // Simulate crash: drop without truncating WAL.
        }

        // Phase 2: Recovery — reopen both.
        {
            let dir = SingleFileDirectory::open(&dir_path).unwrap();

            // Committed segment intact.
            assert_eq!(dir.segments().len(), 1);
            assert_eq!(
                dir.read_segment(SegmentId::new(1)).unwrap(),
                committed_segment_data
            );

            // WAL replay recovers all uncommitted records.
            let records = replay_wal(&wal_path).unwrap();
            assert_eq!(records.len(), 101); // 100 puts + 1 delete

            // Verify first and last records.
            assert_eq!(
                records[0],
                WalRecord::Put {
                    doc_id: DocId::new(0),
                    data: b"{\"title\": \"doc-0\"}".to_vec(),
                }
            );
            assert_eq!(
                records[100],
                WalRecord::Delete {
                    doc_id: DocId::new(50),
                }
            );

            // After processing, truncate WAL (as a real commit would).
            let mut wal = Wal::open(&wal_path, DurabilityMode::Batch).unwrap();
            wal.truncate().unwrap();
        }

        // Phase 3: After truncation, WAL is empty.
        {
            let records = replay_wal(&wal_path).unwrap();
            assert!(records.is_empty());
        }

        let _ = fs::remove_file(&dir_path);
        let _ = fs::remove_file(&wal_path);
    }

    /// WAL replay handles crash mid-write: truncated records are discarded,
    /// complete records are preserved.
    #[test]
    fn wal_replay_survives_crash_mid_write() {
        let wal_path = test_dir().join("wal_crash_mid.wal");
        let _ = fs::remove_file(&wal_path);

        // Write valid records.
        {
            let mut wal = Wal::open(&wal_path, DurabilityMode::Full).unwrap();
            for i in 0..10 {
                wal.append(&WalRecord::Put {
                    doc_id: DocId::new(i),
                    data: format!("doc-{i}").into_bytes(),
                })
                .unwrap();
            }
        }

        // Simulate crash mid-write by appending partial garbage.
        {
            use std::io::Write;
            let mut file = fs::OpenOptions::new()
                .append(true)
                .open(&wal_path)
                .unwrap();
            // Write a partial header (less than 16 bytes).
            file.write_all(&[0xFF; 10]).unwrap();
        }

        // Replay should recover all 10 valid records.
        let records = replay_wal(&wal_path).unwrap();
        assert_eq!(records.len(), 10);
        for (i, record) in records.iter().enumerate() {
            assert_eq!(
                *record,
                WalRecord::Put {
                    doc_id: DocId::new(i as u32),
                    data: format!("doc-{i}").into_bytes(),
                }
            );
        }

        let _ = fs::remove_file(&wal_path);
    }

    /// Multiple crash-replay cycles: WAL truncation and reuse works correctly.
    #[test]
    fn wal_multiple_crash_replay_cycles() {
        let dir_path = test_dir().join("wal_cycles.luci");
        let wal_path = test_dir().join("wal_cycles.luci.wal");
        let _ = fs::remove_file(&dir_path);
        let _ = fs::remove_file(&wal_path);

        let mut dir = SingleFileDirectory::create(&dir_path).unwrap();

        for cycle in 0..5 {
            // Write WAL records.
            {
                let mut wal = Wal::open(&wal_path, DurabilityMode::Full).unwrap();
                for i in 0..20 {
                    wal.append(&WalRecord::Put {
                        doc_id: DocId::new(cycle * 100 + i),
                        data: format!("cycle-{cycle}-doc-{i}").into_bytes(),
                    })
                    .unwrap();
                }
            }

            // Replay and verify.
            let records = replay_wal(&wal_path).unwrap();
            assert_eq!(records.len(), 20);

            // "Commit" by writing segment and truncating WAL.
            let seg_data: Vec<u8> = records
                .iter()
                .flat_map(|r| match r {
                    WalRecord::Put { data, .. } => data.clone(),
                    _ => vec![],
                })
                .collect();

            dir.write_segment(SegmentId::new(cycle as u64), &seg_data)
                .unwrap();
            dir.commit().unwrap();

            let mut wal = Wal::open(&wal_path, DurabilityMode::Batch).unwrap();
            wal.truncate().unwrap();
        }

        // Verify all committed segments are intact.
        assert_eq!(dir.segments().len(), 5);
        assert_eq!(dir.generation(), 5);

        let _ = fs::remove_file(&dir_path);
        let _ = fs::remove_file(&wal_path);
    }
}

#[cfg(unix)]
mod free_list_reclamation {
    use std::fs;
    use std::path::PathBuf;

    use luci_core::SegmentId;
    use luci_storage::SingleFileDirectory;

    fn test_dir() -> PathBuf {
        let dir = std::env::temp_dir()
            .join(format!("luci_exit_freelist_{}", std::process::id()));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    /// Metadata block reclamation: from the 3rd commit onward, the old
    /// inactive root's metadata block is freed and immediately reused for
    /// the new metadata. File growth per commit drops from 2 blocks (segment
    /// + metadata) to 1 block (segment only).
    #[test]
    fn metadata_blocks_are_reclaimed() {
        let path = test_dir().join("meta_reclaim.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();

        // Commits 1-2: both root pointers get populated. No metadata block
        // freed yet (inactive root starts empty). Each commit adds 2 blocks
        // (1 segment + 1 metadata).
        dir.write_segment(SegmentId::new(1), &vec![0xAA; 1000])
            .unwrap();
        dir.commit().unwrap();
        let total_after_first = dir.total_blocks(); // 2 blocks

        dir.write_segment(SegmentId::new(2), &vec![0xBB; 1000])
            .unwrap();
        dir.commit().unwrap();
        let total_after_second = dir.total_blocks(); // 4 blocks
        let growth_second = total_after_second - total_after_first; // 2

        // Commit 3: frees commit 1's metadata block, immediately reuses it.
        // Only 1 new block allocated (segment data).
        dir.write_segment(SegmentId::new(3), &vec![0xCC; 1000])
            .unwrap();
        dir.commit().unwrap();
        let total_after_third = dir.total_blocks();
        let growth_third = total_after_third - total_after_second;

        // Reclamation in action: third commit grows by less than second.
        // Second: +2 (segment + metadata). Third: +1 (segment only, metadata reused).
        assert!(
            growth_third < growth_second,
            "expected reclamation to reduce growth: second +{growth_second}, third +{growth_third}"
        );

        // The freed metadata block was immediately reused, so free_block_count
        // is 0 — but the file didn't grow by an extra metadata block.
        assert_eq!(growth_third, 1, "third commit should grow by 1 block (segment only)");

        // All segments readable.
        for i in 1..=3 {
            dir.read_segment(SegmentId::new(i)).unwrap();
        }

        let _ = fs::remove_file(&path);
    }

    /// Many commits: file growth is linear in segments, not 2x from metadata
    /// accumulation. Proves metadata block reuse works end to end.
    #[test]
    fn many_commits_file_size_stable() {
        let path = test_dir().join("many_commits.luci");
        let _ = fs::remove_file(&path);

        let mut dir = SingleFileDirectory::create(&path).unwrap();

        // Do 20 commits with small segments.
        for i in 0..20 {
            let data = vec![(i as u8).wrapping_mul(0x31); 500];
            dir.write_segment(SegmentId::new(i), &data).unwrap();
            dir.commit().unwrap();
        }

        // With reclamation: 20 segment blocks + 2 metadata blocks = 22.
        // (Commits 1-2 each add segment+metadata. Commits 3-20 each add
        // segment only, reusing the freed metadata block.)
        //
        // Without reclamation: 20 segments + 20 metadata = 40 blocks.
        let total = dir.total_blocks();
        assert_eq!(
            total, 22,
            "expected 22 total blocks (20 segments + 2 metadata), got {total}"
        );

        // All 20 segments must be readable.
        for i in 0..20 {
            let data = dir.read_segment(SegmentId::new(i)).unwrap();
            let expected = vec![(i as u8).wrapping_mul(0x31); 500];
            assert_eq!(data, expected, "segment {i} data mismatch");
        }

        let _ = fs::remove_file(&path);
    }

    /// Reopen preserves allocator state: total_blocks and free list survive
    /// across sessions. New commits after reopen continue the reclamation
    /// pattern.
    #[test]
    fn allocator_state_persists_across_reopen() {
        let path = test_dir().join("alloc_reopen.luci");
        let _ = fs::remove_file(&path);

        let total_before;
        {
            let mut dir = SingleFileDirectory::create(&path).unwrap();
            for i in 0..5 {
                dir.write_segment(SegmentId::new(i), &vec![0xDD; 1000])
                    .unwrap();
                dir.commit().unwrap();
            }
            total_before = dir.total_blocks();
        }

        {
            let mut dir = SingleFileDirectory::open(&path).unwrap();
            assert_eq!(
                dir.total_blocks(),
                total_before,
                "total_blocks not preserved across reopen"
            );

            // All segments still readable.
            for i in 0..5 {
                let data = dir.read_segment(SegmentId::new(i)).unwrap();
                assert_eq!(data, vec![0xDD; 1000]);
            }

            // A new commit after reopen should continue the reclamation
            // pattern: only 1 new block (segment), metadata block reused.
            let before = dir.total_blocks();
            dir.write_segment(SegmentId::new(5), &vec![0xEE; 1000])
                .unwrap();
            dir.commit().unwrap();
            let growth = dir.total_blocks() - before;
            assert_eq!(
                growth, 1,
                "post-reopen commit should grow by 1 block, grew by {growth}"
            );
        }

        let _ = fs::remove_file(&path);
    }
}

#[cfg(unix)]
mod fs_directory_equivalence {
    use std::fs;
    use std::path::PathBuf;

    use luci_core::SegmentId;
    use luci_storage::{FsDirectory, SingleFileDirectory};

    fn test_dir() -> PathBuf {
        let dir = std::env::temp_dir()
            .join(format!("luci_exit_equiv_{}", std::process::id()));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    /// Property test: FsDirectory and SingleFileDirectory produce the same
    /// logical results for all operations.
    #[test]
    fn both_backends_produce_same_results() {
        let sf_path = test_dir().join("equiv_sf.luci");
        let fs_path = test_dir().join("equiv_fs");
        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);

        let mut sf = SingleFileDirectory::create(&sf_path).unwrap();
        let mut fs_dir = FsDirectory::create(&fs_path).unwrap();

        // Sequence of operations to test.
        let ops: Vec<(u64, Vec<u8>)> = vec![
            (1, b"hello world".to_vec()),
            (2, b"another segment with more data".to_vec()),
            (3, vec![0xAB; 10_000]),
            (4, vec![0xFF; 1]),
            (5, b"final segment".to_vec()),
        ];

        // Single commit with multiple segments.
        for (id, data) in &ops[..2] {
            sf.write_segment(SegmentId::new(*id), data).unwrap();
            fs_dir.write_segment(SegmentId::new(*id), data).unwrap();
        }
        sf.commit().unwrap();
        fs_dir.commit().unwrap();

        assert_eq!(sf.generation(), fs_dir.generation());
        assert_eq!(sf.segments().len(), fs_dir.segments().len());

        // Separate commits.
        for (id, data) in &ops[2..] {
            sf.write_segment(SegmentId::new(*id), data).unwrap();
            fs_dir.write_segment(SegmentId::new(*id), data).unwrap();
            sf.commit().unwrap();
            fs_dir.commit().unwrap();
        }

        // Compare.
        assert_eq!(sf.generation(), fs_dir.generation());
        assert_eq!(sf.segments().len(), fs_dir.segments().len());

        for (sf_entry, fs_entry) in sf.segments().iter().zip(fs_dir.segments().iter()) {
            assert_eq!(sf_entry.segment_id, fs_entry.segment_id);
            assert_eq!(sf_entry.data_len, fs_entry.data_len);
            assert_eq!(sf_entry.generation, fs_entry.generation);

            let sf_data = sf.read_segment(sf_entry.segment_id).unwrap();
            let fs_data = fs_dir.read_segment(fs_entry.segment_id).unwrap();
            assert_eq!(sf_data, fs_data);
        }

        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);
    }

    /// Property test with randomized operations.
    #[test]
    fn randomized_equivalence() {
        use rand::Rng;

        let sf_path = test_dir().join("rand_equiv_sf.luci");
        let fs_path = test_dir().join("rand_equiv_fs");
        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);

        let mut sf = SingleFileDirectory::create(&sf_path).unwrap();
        let mut fs_dir = FsDirectory::create(&fs_path).unwrap();

        let mut rng = rand::thread_rng();
        let mut seg_id = 0u64;

        for _ in 0..20 {
            // Write 1-5 segments.
            let count = rng.gen_range(1..=5);
            for _ in 0..count {
                let size = rng.gen_range(1..50_000);
                let data: Vec<u8> = (0..size).map(|_| rng.r#gen()).collect();
                sf.write_segment(SegmentId::new(seg_id), &data).unwrap();
                fs_dir.write_segment(SegmentId::new(seg_id), &data).unwrap();
                seg_id += 1;
            }
            sf.commit().unwrap();
            fs_dir.commit().unwrap();
        }

        // Compare all results.
        assert_eq!(sf.generation(), fs_dir.generation());
        assert_eq!(sf.segments().len(), fs_dir.segments().len());

        for (sf_entry, fs_entry) in sf.segments().iter().zip(fs_dir.segments().iter()) {
            assert_eq!(sf_entry.segment_id, fs_entry.segment_id);
            assert_eq!(sf_entry.data_len, fs_entry.data_len);
            assert_eq!(sf_entry.generation, fs_entry.generation);

            let sf_data = sf.read_segment(sf_entry.segment_id).unwrap();
            let fs_data = fs_dir.read_segment(fs_entry.segment_id).unwrap();
            assert_eq!(sf_data, fs_data);
        }

        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);
    }

    /// Both backends handle empty commits the same way.
    #[test]
    fn empty_commit_equivalence() {
        let sf_path = test_dir().join("empty_equiv_sf.luci");
        let fs_path = test_dir().join("empty_equiv_fs");
        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);

        let mut sf = SingleFileDirectory::create(&sf_path).unwrap();
        let mut fs_dir = FsDirectory::create(&fs_path).unwrap();

        // Empty commit should be a no-op for both.
        sf.commit().unwrap();
        fs_dir.commit().unwrap();
        assert_eq!(sf.generation(), fs_dir.generation());
        assert_eq!(sf.generation(), 0);

        // Write, commit, empty commit.
        sf.write_segment(SegmentId::new(1), b"data").unwrap();
        fs_dir.write_segment(SegmentId::new(1), b"data").unwrap();
        sf.commit().unwrap();
        fs_dir.commit().unwrap();

        sf.commit().unwrap();
        fs_dir.commit().unwrap();
        assert_eq!(sf.generation(), fs_dir.generation());
        assert_eq!(sf.generation(), 1);

        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);
    }

    /// Both backends handle reopen the same way.
    #[test]
    fn reopen_equivalence() {
        let sf_path = test_dir().join("reopen_equiv_sf.luci");
        let fs_path = test_dir().join("reopen_equiv_fs");
        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);

        {
            let mut sf = SingleFileDirectory::create(&sf_path).unwrap();
            let mut fs_dir = FsDirectory::create(&fs_path).unwrap();

            for i in 0..5 {
                let data = format!("segment-data-{i}").into_bytes();
                sf.write_segment(SegmentId::new(i), &data).unwrap();
                fs_dir.write_segment(SegmentId::new(i), &data).unwrap();
            }
            sf.commit().unwrap();
            fs_dir.commit().unwrap();

            // Write uncommitted data.
            sf.write_segment(SegmentId::new(99), b"uncommitted")
                .unwrap();
            fs_dir
                .write_segment(SegmentId::new(99), b"uncommitted")
                .unwrap();
        }

        // Reopen both.
        let sf = SingleFileDirectory::open(&sf_path).unwrap();
        let fs_dir = FsDirectory::open(&fs_path).unwrap();

        assert_eq!(sf.generation(), fs_dir.generation());
        assert_eq!(sf.segments().len(), fs_dir.segments().len());
        assert_eq!(sf.segments().len(), 5);

        // Uncommitted segment not visible.
        assert!(sf.read_segment(SegmentId::new(99)).is_err());
        assert!(fs_dir.read_segment(SegmentId::new(99)).is_err());

        // Committed segments intact.
        for i in 0..5 {
            let expected = format!("segment-data-{i}").into_bytes();
            assert_eq!(sf.read_segment(SegmentId::new(i)).unwrap(), expected);
            assert_eq!(fs_dir.read_segment(SegmentId::new(i)).unwrap(), expected);
        }

        let _ = fs::remove_file(&sf_path);
        let _ = fs::remove_dir_all(&fs_path);
    }
}
