#![cfg(feature = "typed-tree")]

use armdb::codec::Codec;
use armdb::{Config, DbError, TypedMap, TypedTree, TypedWriteHook};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::tempdir;

struct U64Codec;

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

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

// ---------------------------------------------------------------------------
// V7: atomic() must NOT fire hooks
// ---------------------------------------------------------------------------

struct CountingHook {
    write_count: Arc<AtomicUsize>,
}

impl TypedWriteHook<[u8; 8], u64> for CountingHook {
    const NEEDS_OLD_VALUE: bool = true;
    const NEEDS_INIT: bool = false;

    fn on_write(&self, _key: &[u8; 8], _old: Option<&u64>, _new: Option<&u64>) {
        self.write_count.fetch_add(1, Ordering::Relaxed);
    }
}

#[test]
fn v7_atomic_fires_hooks_tree() {
    let dir = tempdir().unwrap();
    let count = Arc::new(AtomicUsize::new(0));
    let hook = CountingHook {
        write_count: Arc::clone(&count),
    };
    let tree = TypedTree::<[u8; 8], u64, U64Codec, _>::open_hooked(
        dir.path(),
        Config::test(),
        U64Codec,
        hook,
    )
    .unwrap();

    let key = 1u64.to_be_bytes();
    tree.put(&key, 100).unwrap();
    let before = count.load(Ordering::Relaxed);
    assert!(before > 0, "hook should fire on direct put");

    count.store(0, Ordering::Relaxed);

    tree.atomic(&key, |shard| {
        shard.put(&key, 200)?;
        shard.delete(&key)?;
        shard.insert(&key, 300)?;
        Ok(())
    })
    .unwrap();

    let after = count.load(Ordering::Relaxed);
    assert_eq!(
        after, 3,
        "hooks fire inside atomic() in application order; got {after}"
    );
}

#[test]
fn v7_atomic_fires_hooks_map() {
    let dir = tempdir().unwrap();
    let count = Arc::new(AtomicUsize::new(0));
    let hook = CountingHook {
        write_count: Arc::clone(&count),
    };
    let map = TypedMap::<[u8; 8], u64, U64Codec, _>::open_hooked(
        dir.path(),
        Config::test(),
        U64Codec,
        hook,
    )
    .unwrap();

    let key = 1u64.to_be_bytes();
    map.put(&key, 100).unwrap();
    let before = count.load(Ordering::Relaxed);
    assert!(before > 0, "hook should fire on direct put");

    count.store(0, Ordering::Relaxed);

    map.atomic(&key, |shard| {
        shard.put(&key, 200)?;
        shard.delete(&key)?;
        shard.insert(&key, 300)?;
        Ok(())
    })
    .unwrap();

    let after = count.load(Ordering::Relaxed);
    assert_eq!(
        after, 3,
        "hooks fire inside atomic() in application order; got {after}"
    );
}

// ---------------------------------------------------------------------------
// V8: prefix_iter with oversized prefix returns empty
// ---------------------------------------------------------------------------

#[test]
fn v8_prefix_iter_oversized_returns_empty() {
    let dir = tempdir().unwrap();
    let tree =
        TypedTree::<[u8; 8], u64, U64Codec>::open(dir.path(), Config::test(), U64Codec).unwrap();

    tree.put(&1u64.to_be_bytes(), 100).unwrap();

    let oversized_prefix = vec![0u8; 16];
    let items: Vec<_> = tree.prefix_iter(&oversized_prefix).collect();
    assert!(
        items.is_empty(),
        "oversized prefix must return empty iterator"
    );
}

// ---------------------------------------------------------------------------
// V9: TypedMap::len() accuracy after put/insert/delete
// ---------------------------------------------------------------------------

#[test]
fn v9_typed_map_len_accuracy() {
    let dir = tempdir().unwrap();
    let map =
        TypedMap::<[u8; 8], u64, U64Codec>::open(dir.path(), Config::test(), U64Codec).unwrap();

    assert_eq!(map.len(), 0);
    assert!(map.is_empty());

    map.put(&1u64.to_be_bytes(), 100).unwrap();
    assert_eq!(map.len(), 1);

    map.put(&2u64.to_be_bytes(), 200).unwrap();
    assert_eq!(map.len(), 2);

    // put on existing key — len stays same
    map.put(&1u64.to_be_bytes(), 999).unwrap();
    assert_eq!(map.len(), 2);

    map.insert(&3u64.to_be_bytes(), 300).unwrap();
    assert_eq!(map.len(), 3);

    // insert on existing key — error, len unchanged
    assert!(map.insert(&1u64.to_be_bytes(), 111).is_err());
    assert_eq!(map.len(), 3);

    map.delete(&2u64.to_be_bytes()).unwrap();
    assert_eq!(map.len(), 2);

    // delete non-existent — len unchanged
    map.delete(&99u64.to_be_bytes()).unwrap();
    assert_eq!(map.len(), 2);

    map.delete(&1u64.to_be_bytes()).unwrap();
    map.delete(&3u64.to_be_bytes()).unwrap();
    assert_eq!(map.len(), 0);
    assert!(map.is_empty());
}

#[test]
fn v9_typed_map_len_survives_reopen() {
    let dir = tempdir().unwrap();
    {
        let map =
            TypedMap::<[u8; 8], u64, U64Codec>::open(dir.path(), Config::test(), U64Codec).unwrap();
        map.put(&1u64.to_be_bytes(), 100).unwrap();
        map.put(&2u64.to_be_bytes(), 200).unwrap();
        map.put(&3u64.to_be_bytes(), 300).unwrap();
        map.close().unwrap();
    }
    {
        let map =
            TypedMap::<[u8; 8], u64, U64Codec>::open(dir.path(), Config::test(), U64Codec).unwrap();
        assert_eq!(map.len(), 3, "len must be correct after recovery");
    }
}

// ---------------------------------------------------------------------------
// V9: TypedMap::len() accuracy inside atomic()
// ---------------------------------------------------------------------------

#[test]
fn v9_typed_map_len_atomic() {
    let dir = tempdir().unwrap();
    let map =
        TypedMap::<[u8; 8], u64, U64Codec>::open(dir.path(), Config::test(), U64Codec).unwrap();

    let key = 1u64.to_be_bytes();

    // Use put + delete on the same key inside atomic (same shard guaranteed)
    map.atomic(&key, |shard| {
        shard.put(&key, 100)?;
        Ok(())
    })
    .unwrap();

    assert_eq!(map.len(), 1);

    map.atomic(&key, |shard| {
        shard.delete(&key)?;
        Ok(())
    })
    .unwrap();

    assert_eq!(map.len(), 0);
}
