//! Shared `#[cfg(test)]` fault-injection helpers for the read/mutate error
//! contract tests (spec 26-06-13-armdb-var-read-error-contract).

#[cfg(feature = "encryption")]
use std::path::Path;

use crate::codec::Codec;
use crate::error::{DbError, DbResult};

/// Sentinel `u64` whose bytes are *readable* but which `PoisonCodec` refuses to
/// *decode* — simulates a value corrupted/drifted at the codec layer.
pub(crate) const POISON: u64 = 0xDEAD_BEEF_DEAD_BEEF;

/// Test codec for `u64`: stores the little-endian bytes verbatim, but
/// `decode_from` returns `CorruptedEntry` for the `POISON` sentinel (and for any
/// non-8-byte input). Lets a test write a value whose bytes read back fine yet
/// fail to decode.
#[derive(Clone, Default)]
pub(crate) struct PoisonCodec;

impl Codec<u64> for PoisonCodec {
    fn encode_to(&self, value: &u64, buf: &mut Vec<u8>) -> DbResult<()> {
        buf.clear();
        buf.extend_from_slice(&value.to_le_bytes());
        Ok(())
    }

    fn decode_from(&self, bytes: &[u8]) -> DbResult<u64> {
        let arr: [u8; 8] = bytes
            .try_into()
            .map_err(|_| DbError::CorruptedEntry { offset: 0 })?;
        let v = u64::from_le_bytes(arr);
        if v == POISON {
            return Err(DbError::CorruptedEntry { offset: 0 });
        }
        Ok(v)
    }

    fn size(&self, _value: &u64) -> usize {
        8
    }
}

/// A value large enough (16 KiB) to force the on-disk read path after a file
/// rotation, matching the existing encrypted large-value tests.
/// Only consumed by the `encryption`-gated raw read-fault tests.
#[cfg(feature = "encryption")]
pub(crate) fn big_value(fill: u8) -> Vec<u8> {
    vec![fill; 16 * 1024]
}

/// Corrupt every AEAD tag file (`*.tags`) in `shard_dir` by flipping all bytes.
/// With `encryption` enabled, each data page has a sibling tag; flipping tags
/// makes the next decrypt of any page fail with `EncryptionError`. Call after a
/// rotation (so the value sits in an immutable on-disk file) and with the block
/// cache disabled (the default `Config::test()`), so the next read decrypts from
/// disk. Only consumed by the `encryption`-gated raw read-fault tests.
#[cfg(feature = "encryption")]
pub(crate) fn corrupt_tags(shard_dir: &Path) {
    let mut found = false;
    for entry in std::fs::read_dir(shard_dir).expect("read shard dir") {
        let path = entry.expect("dir entry").path();
        if path.extension().and_then(|e| e.to_str()) == Some("tags") {
            let mut bytes = std::fs::read(&path).expect("read tags file");
            if bytes.is_empty() {
                continue;
            }
            for b in bytes.iter_mut() {
                *b ^= 0xFF;
            }
            std::fs::write(&path, &bytes).expect("write tags file");
            found = true;
        }
    }
    assert!(found, "no non-empty .tags file found in {shard_dir:?}");
}
