#[cfg(feature = "var-collections")]
use armdb::VarTree;
use armdb::{Config, ConstTree};
use std::fs;
use std::path::Path;
use tempfile::tempdir;

/// Find the .data file(s) in a shard directory, sorted by name.
fn find_data_files(shard_dir: &Path) -> Vec<std::path::PathBuf> {
    let mut files: Vec<_> = fs::read_dir(shard_dir)
        .unwrap()
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("data"))
        .collect();
    files.sort();
    files
}

fn config_1_shard() -> Config {
    let mut c = Config::test();
    c.shard_count = 1;
    c
}

/// Remove all .hint files in a shard directory so recovery does a full scan.
fn remove_hint_files(shard_dir: &Path) {
    for entry in fs::read_dir(shard_dir).unwrap().filter_map(|e| e.ok()) {
        if entry.path().extension().and_then(|e| e.to_str()) == Some("hint") {
            fs::remove_file(entry.path()).unwrap();
        }
    }
}

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

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config_1_shard()).unwrap();

        for i in 0..100u64 {
            let key = i.to_be_bytes();
            let val = (i * 10).to_be_bytes();
            tree.put(&key, &val).unwrap();
        }

        tree.close().unwrap();
    }

    // Truncate the data file by a few bytes (corrupting the last entry)
    let shard_dir = dir.path().join("shard_000");
    let data_files = find_data_files(&shard_dir);
    assert!(!data_files.is_empty(), "no data files found");

    let data_file = &data_files[data_files.len() - 1];
    let original_len = fs::metadata(data_file).unwrap().len();
    assert!(original_len > 10, "data file too small to truncate");

    remove_hint_files(&shard_dir);

    let file = fs::OpenOptions::new().write(true).open(data_file).unwrap();
    file.set_len(original_len - 10).unwrap();
    drop(file);

    // Reopen — should recover all complete entries
    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config_1_shard()).unwrap();
        assert!(
            tree.len() >= 99,
            "expected at least 99 entries after truncation, got {}",
            tree.len()
        );

        for i in 0..100u64 {
            let key = i.to_be_bytes();
            if let Some(val) = tree.get(&key) {
                let expected = (i * 10).to_be_bytes();
                assert_eq!(val, expected, "value mismatch at key {}", i);
            }
        }
    }
}

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

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config_1_shard()).unwrap();

        for i in 0..100u64 {
            let key = i.to_be_bytes();
            let val = (i * 10).to_be_bytes();
            tree.put(&key, &val).unwrap();
        }

        tree.close().unwrap();
    }

    // Truncate all data files to 0 bytes + remove hint files
    let shard_dir = dir.path().join("shard_000");
    remove_hint_files(&shard_dir);
    for file_path in find_data_files(&shard_dir) {
        let file = fs::OpenOptions::new().write(true).open(&file_path).unwrap();
        file.set_len(0).unwrap();
    }

    // Reopen — should have 0 entries
    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config_1_shard()).unwrap();
        assert_eq!(tree.len(), 0);
    }
}

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

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config_1_shard()).unwrap();

        for i in 0..100u64 {
            let key = i.to_be_bytes();
            let val = (i * 10).to_be_bytes();
            tree.put(&key, &val).unwrap();
        }

        tree.close().unwrap();
    }

    let shard_dir = dir.path().join("shard_000");
    let data_files = find_data_files(&shard_dir);
    let data_file = &data_files[data_files.len() - 1];
    let original_len = fs::metadata(data_file).unwrap().len();

    remove_hint_files(&shard_dir);

    // Entry size for [u8;8] key + [u8;8] value: header(16) + key(8) + value(8) = 32 bytes
    // Truncate to leave a partial header (< 16 bytes) at the end
    let partial_header_len = original_len - (original_len % 32) + 8;
    let truncate_to = if partial_header_len < original_len {
        partial_header_len
    } else {
        original_len - 8
    };

    let file = fs::OpenOptions::new().write(true).open(data_file).unwrap();
    file.set_len(truncate_to).unwrap();
    drop(file);

    // Reopen — all complete entries should recover
    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config_1_shard()).unwrap();
        assert!(
            tree.len() >= 98,
            "expected at least 98 entries after header truncation, got {}",
            tree.len()
        );
    }
}

#[cfg(feature = "var-collections")]
#[test]
fn test_recovery_var_tree_truncated() {
    let dir = tempdir().unwrap();

    {
        let tree = VarTree::<[u8; 8]>::open(dir.path(), config_1_shard()).unwrap();

        for i in 0..100u64 {
            let key = i.to_be_bytes();
            let val = format!("value_{i}");
            tree.put(&key, val.as_bytes()).unwrap();
        }

        tree.close().unwrap();
    }

    let shard_dir = dir.path().join("shard_000");
    let data_files = find_data_files(&shard_dir);
    let data_file = &data_files[data_files.len() - 1];
    let original_len = fs::metadata(data_file).unwrap().len();

    remove_hint_files(&shard_dir);

    let file = fs::OpenOptions::new().write(true).open(data_file).unwrap();
    file.set_len(original_len - 10).unwrap();
    drop(file);

    // Reopen — should recover most entries
    {
        let tree = VarTree::<[u8; 8]>::open(dir.path(), config_1_shard()).unwrap();
        assert!(
            tree.len() >= 99,
            "expected at least 99 entries after truncation, got {}",
            tree.len()
        );

        for i in 0..100u64 {
            let key = i.to_be_bytes();
            if let Some(val) = tree.get(&key) {
                let expected = format!("value_{i}");
                assert_eq!(
                    val.as_ref(),
                    expected.as_bytes(),
                    "value mismatch at key {}",
                    i
                );
            }
        }
    }
}
