#![cfg(feature = "rapira-codec")]

use armdb::{Config, MigrateAction, RapiraCodec, TypedTree};
use rapira::Rapira;
use tempfile::tempdir;

// ---------------------------------------------------------------------------
// Test types
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Rapira)]
struct User {
    id: u64,
    name: String,
    email: String,
    age: u32,
}

#[derive(Debug, Clone, PartialEq, Rapira)]
struct Post {
    author_id: u64,
    title: String,
    body: String,
    likes: u64,
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

type UserTree = TypedTree<[u8; 16], User, RapiraCodec>;

fn open_user_tree(dir: &std::path::Path) -> UserTree {
    TypedTree::open(dir, Config::test(), RapiraCodec).unwrap()
}

fn make_user(id: u64) -> User {
    User {
        id,
        name: format!("user_{id}"),
        email: format!("user_{id}@test.com"),
        age: 20 + (id % 50) as u32,
    }
}

fn user_key(id: u64) -> [u8; 16] {
    let mut key = [0u8; 16];
    key[8..].copy_from_slice(&id.to_be_bytes());
    key
}

// ---------------------------------------------------------------------------
// Basic CRUD with rapira
// ---------------------------------------------------------------------------

#[test]
fn test_rapira_put_get() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    let user = make_user(1);
    tree.put(&user_key(1), user.clone()).unwrap();

    let r = tree.get(&user_key(1)).unwrap();
    assert_eq!(r.id, 1);
    assert_eq!(r.name, "user_1");
    assert_eq!(r.email, "user_1@test.com");
    assert_eq!(r.age, 21);
}

#[test]
fn test_rapira_put_returns_old() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    let u1 = make_user(1);
    let old = tree.put(&user_key(1), u1).unwrap();
    assert!(old.is_none());

    let u1_updated = User {
        id: 1,
        name: "updated".into(),
        email: "new@test.com".into(),
        age: 30,
    };
    let old = tree.put(&user_key(1), u1_updated).unwrap().unwrap();
    assert_eq!(old.name, "user_1");
    assert_eq!(old.age, 21);

    let current = tree.get(&user_key(1)).unwrap();
    assert_eq!(current.name, "updated");
}

#[test]
fn test_rapira_delete() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    tree.put(&user_key(1), make_user(1)).unwrap();
    let old = tree.delete(&user_key(1)).unwrap().unwrap();
    assert_eq!(old.id, 1);
    assert!(tree.get(&user_key(1)).is_none());
}

#[test]
fn test_rapira_insert_duplicate() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    tree.insert(&user_key(1), make_user(1)).unwrap();
    let err = tree.insert(&user_key(1), make_user(1)).unwrap_err();
    assert!(matches!(err, armdb::DbError::KeyExists));
}

// ---------------------------------------------------------------------------
// CAS / Update
// ---------------------------------------------------------------------------

#[test]
fn test_rapira_cas() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    let u1 = make_user(1);
    tree.put(&user_key(1), u1.clone()).unwrap();

    let u1_new = User {
        age: 99,
        ..u1.clone()
    };

    // Mismatch
    let wrong = User {
        age: 0,
        ..u1.clone()
    };
    assert!(tree.cas(&user_key(1), &wrong, u1_new.clone()).is_err());

    // Match
    tree.cas(&user_key(1), &u1, u1_new).unwrap();
    assert_eq!(tree.get(&user_key(1)).unwrap().age, 99);
}

#[test]
fn test_rapira_update() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    tree.put(&user_key(1), make_user(1)).unwrap();

    let r = tree
        .update(&user_key(1), |u| User {
            age: u.age + 10,
            ..u.clone()
        })
        .unwrap()
        .unwrap();

    // Returns ref to new value
    assert_eq!(r.age, 31);
    assert_eq!(tree.get(&user_key(1)).unwrap().age, 31);
}

// ---------------------------------------------------------------------------
// Iteration
// ---------------------------------------------------------------------------

#[test]
fn test_rapira_iter() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    for i in 0..10u64 {
        tree.put(&user_key(i), make_user(i)).unwrap();
    }

    // reversed=true → descending
    let entries: Vec<_> = tree.iter().collect();
    assert_eq!(entries.len(), 10);

    for (i, (_key, user)) in entries.iter().enumerate() {
        assert_eq!(user.id, 9 - i as u64);
    }
}

#[test]
fn test_rapira_iter_rev() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    for i in 0..10u64 {
        tree.put(&user_key(i), make_user(i)).unwrap();
    }

    // .rev() on reversed=true → ascending
    let entries: Vec<_> = tree.iter().rev().collect();
    assert_eq!(entries.len(), 10);
    assert_eq!(entries[0].1.id, 0);
    assert_eq!(entries[9].1.id, 9);
}

// ---------------------------------------------------------------------------
// Prefix iteration (composite key: user_id + post_id)
// ---------------------------------------------------------------------------

fn post_key(user_id: u64, post_id: u64) -> [u8; 16] {
    let mut key = [0u8; 16];
    key[..8].copy_from_slice(&user_id.to_be_bytes());
    key[8..].copy_from_slice(&post_id.to_be_bytes());
    key
}

#[test]
fn test_rapira_prefix_iter_composite_key() {
    let dir = tempdir().unwrap();
    let tree: TypedTree<[u8; 16], Post, RapiraCodec> =
        TypedTree::open(dir.path(), Config::test(), RapiraCodec).unwrap();

    // User 1: 5 posts
    for i in 0..5u64 {
        tree.put(
            &post_key(1, i),
            Post {
                author_id: 1,
                title: format!("post_{i}"),
                body: format!("body_{i}"),
                likes: i * 10,
            },
        )
        .unwrap();
    }

    // User 2: 3 posts
    for i in 0..3u64 {
        tree.put(
            &post_key(2, i),
            Post {
                author_id: 2,
                title: format!("post_{i}"),
                body: format!("body_{i}"),
                likes: i * 5,
            },
        )
        .unwrap();
    }

    // Prefix iter for user 1
    let user1_posts: Vec<_> = tree.prefix_iter(&1u64.to_be_bytes()).collect();
    assert_eq!(user1_posts.len(), 5);
    assert!(user1_posts.iter().all(|(_, p)| p.author_id == 1));

    // Prefix iter for user 2
    let user2_posts: Vec<_> = tree.prefix_iter(&2u64.to_be_bytes()).collect();
    assert_eq!(user2_posts.len(), 3);

    // Prefix iter for non-existent user
    let user99: Vec<_> = tree.prefix_iter(&99u64.to_be_bytes()).collect();
    assert_eq!(user99.len(), 0);
}

#[test]
fn test_rapira_prefix_newest_first() {
    let dir = tempdir().unwrap();
    let mut config = Config::test();
    config.reversed = true;
    let tree: TypedTree<[u8; 16], Post, RapiraCodec> =
        TypedTree::open(dir.path(), config, RapiraCodec).unwrap();

    // User 1: posts 0..20
    for i in 0..20u64 {
        tree.put(
            &post_key(1, i),
            Post {
                author_id: 1,
                title: format!("post_{i}"),
                body: String::new(),
                likes: i,
            },
        )
        .unwrap();
    }

    // Take 5 newest posts
    let latest: Vec<_> = tree.prefix_iter(&1u64.to_be_bytes()).take(5).collect();
    assert_eq!(latest.len(), 5);
    assert_eq!(latest[0].1.likes, 19); // newest first
    assert_eq!(latest[4].1.likes, 15);
}

// ---------------------------------------------------------------------------
// Recovery with rapira
// ---------------------------------------------------------------------------

#[test]
fn test_rapira_recovery() {
    let dir = tempdir().unwrap();

    {
        let tree = open_user_tree(dir.path());
        for i in 0..50u64 {
            tree.put(&user_key(i), make_user(i)).unwrap();
        }
        tree.close().unwrap();
    }

    let tree = open_user_tree(dir.path());
    assert_eq!(tree.len(), 50);

    for i in 0..50u64 {
        let u = tree.get(&user_key(i)).unwrap();
        assert_eq!(u.id, i);
        assert_eq!(u.name, format!("user_{i}"));
    }
}

#[test]
fn test_rapira_recovery_with_updates() {
    let dir = tempdir().unwrap();

    {
        let tree = open_user_tree(dir.path());
        for i in 0..20u64 {
            tree.put(&user_key(i), make_user(i)).unwrap();
        }
        // Update even users
        for i in (0..20u64).step_by(2) {
            tree.put(
                &user_key(i),
                User {
                    age: 99,
                    ..make_user(i)
                },
            )
            .unwrap();
        }
        // Delete first 5
        for i in 0..5u64 {
            tree.delete(&user_key(i)).unwrap();
        }
        tree.close().unwrap();
    }

    let tree = open_user_tree(dir.path());
    assert_eq!(tree.len(), 15);

    // Deleted
    for i in 0..5u64 {
        assert!(tree.get(&user_key(i)).is_none());
    }

    // Updated even keys
    for i in (6..20u64).step_by(2) {
        assert_eq!(tree.get(&user_key(i)).unwrap().age, 99);
    }

    // Unchanged odd keys
    for i in (5..20u64).step_by(2) {
        let u = tree.get(&user_key(i)).unwrap();
        assert_eq!(u.age, 20 + (i % 50) as u32);
    }
}

// ---------------------------------------------------------------------------
// Migrate with rapira
// ---------------------------------------------------------------------------

#[test]
fn test_rapira_migrate() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    for i in 0..10u64 {
        tree.put(&user_key(i), make_user(i)).unwrap();
    }

    let count = tree
        .migrate(|_key, user| {
            if user.age < 25 {
                MigrateAction::Delete
            } else {
                MigrateAction::Keep
            }
        })
        .unwrap();

    // Users with age < 25: id 0 (age 20), 1 (21), 2 (22), 3 (23), 4 (24) = 5 deleted
    assert_eq!(count, 5);
    assert_eq!(tree.len(), 5);
}

// ---------------------------------------------------------------------------
// Compact with rapira
// ---------------------------------------------------------------------------

#[test]
fn test_rapira_compact() {
    let dir = tempdir().unwrap();
    let tree = open_user_tree(dir.path());

    for i in 0..100u64 {
        tree.put(&user_key(i), make_user(i)).unwrap();
    }
    // Overwrite all
    for i in 0..100u64 {
        tree.put(
            &user_key(i),
            User {
                age: 99,
                ..make_user(i)
            },
        )
        .unwrap();
    }

    tree.flush_buffers().unwrap();
    tree.compact().unwrap();

    // Verify integrity
    for i in 0..100u64 {
        let u = tree.get(&user_key(i)).unwrap();
        assert_eq!(u.id, i);
        assert_eq!(u.age, 99);
    }
}

// ---------------------------------------------------------------------------
// prefix_iter with composite zerocopy key — Config::balanced().build() regression
// Reproducer for: armdb-0.1.10-prefix-iter-reversed.md
// ---------------------------------------------------------------------------

#[derive(
    zerocopy::FromBytes, zerocopy::IntoBytes, zerocopy::Immutable, Copy, Clone, Debug, PartialEq,
)]
#[repr(C)]
struct SessionKey {
    user_id: [u8; 8],
    session_id: [u8; 16],
}

armdb::impl_key_zerocopy!(
    SessionKey,
    armour_core::KeyScheme::Typed(&[armour_core::KeyType::Fuid, armour_core::KeyType::Array(16),])
);

fn make_session_key(user: u64, session: u128) -> SessionKey {
    SessionKey {
        user_id: user.to_be_bytes(),
        session_id: session.to_be_bytes(),
    }
}

fn make_post(author_id: u64, likes: u64) -> Post {
    Post {
        author_id,
        title: format!("sess_{likes}"),
        body: String::new(),
        likes,
    }
}

fn populate_sessions(tree: &TypedTree<SessionKey, Post, RapiraCodec>) {
    for s in [100u128, 101, 102] {
        tree.put(&make_session_key(1, s), make_post(1, s as u64))
            .unwrap();
    }
    for s in [200u128, 201] {
        tree.put(&make_session_key(2, s), make_post(2, s as u64))
            .unwrap();
    }
}

// Reproducer: Config::balanced().build() + composite zerocopy key.
// Before the fix: prefix_iter returned 0 items.
#[test]
fn test_prefix_iter_composite_zerocopy_default_config() {
    let dir = tempdir().unwrap();
    let tree: TypedTree<SessionKey, Post, RapiraCodec> =
        TypedTree::open(dir.path(), Config::balanced().build(), RapiraCodec).unwrap();

    populate_sessions(&tree);

    let user1: Vec<_> = tree.prefix_iter(&1u64.to_be_bytes()).collect();
    assert_eq!(user1.len(), 3, "user=1: expected 3 sessions");
    assert!(user1.iter().all(|(_, p)| p.author_id == 1));

    let user2: Vec<_> = tree.prefix_iter(&2u64.to_be_bytes()).collect();
    assert_eq!(user2.len(), 2, "user=2: expected 2 sessions");

    let user99: Vec<_> = tree.prefix_iter(&99u64.to_be_bytes()).collect();
    assert_eq!(user99.len(), 0, "user=99: expected 0 sessions");
}

// Config bisect: same key, Config::test() (shard_count=2).
// If this passes and the default-config test fails, the bug is shard_count-specific.
#[test]
fn test_prefix_iter_composite_zerocopy_test_config() {
    let dir = tempdir().unwrap();
    let tree: TypedTree<SessionKey, Post, RapiraCodec> =
        TypedTree::open(dir.path(), Config::test(), RapiraCodec).unwrap();

    populate_sessions(&tree);

    let user1: Vec<_> = tree.prefix_iter(&1u64.to_be_bytes()).collect();
    assert_eq!(user1.len(), 3, "user=1: expected 3 sessions");

    let user2: Vec<_> = tree.prefix_iter(&2u64.to_be_bytes()).collect();
    assert_eq!(user2.len(), 2, "user=2: expected 2 sessions");
}

// Key type bisect: plain [u8; 24], Config::balanced().build().
// If this passes and the composite-key test fails, bug is KeyScheme-specific.
#[test]
fn test_prefix_iter_plain_24byte_default_config() {
    let dir = tempdir().unwrap();
    let tree: TypedTree<[u8; 24], Post, RapiraCodec> =
        TypedTree::open(dir.path(), Config::balanced().build(), RapiraCodec).unwrap();

    let make_key = |user: u64, session: u128| -> [u8; 24] {
        let mut k = [0u8; 24];
        k[..8].copy_from_slice(&user.to_be_bytes());
        k[8..].copy_from_slice(&session.to_be_bytes());
        k
    };

    for s in [100u128, 101, 102] {
        tree.put(&make_key(1, s), make_post(1, s as u64)).unwrap();
    }
    for s in [200u128, 201] {
        tree.put(&make_key(2, s), make_post(2, s as u64)).unwrap();
    }

    let user1: Vec<_> = tree.prefix_iter(&1u64.to_be_bytes()).collect();
    assert_eq!(user1.len(), 3, "user=1: expected 3 entries");

    let user2: Vec<_> = tree.prefix_iter(&2u64.to_be_bytes()).collect();
    assert_eq!(user2.len(), 2, "user=2: expected 2 entries");
}

// Ordering: Config::balanced().build() + composite key, reversed=true (newest first).
#[test]
fn test_prefix_iter_composite_newest_first_default_config() {
    let dir = tempdir().unwrap();
    let tree: TypedTree<SessionKey, Post, RapiraCodec> =
        TypedTree::open(dir.path(), Config::balanced().build(), RapiraCodec).unwrap();

    for s in 1u128..=20 {
        tree.put(&make_session_key(1, s), make_post(1, s as u64))
            .unwrap();
    }

    let latest: Vec<_> = tree.prefix_iter(&1u64.to_be_bytes()).take(5).collect();
    assert_eq!(latest.len(), 5);
    assert_eq!(latest[0].1.likes, 20, "newest first");
    assert_eq!(latest[4].1.likes, 16);
}
