use armdb::{Config, ConstTree};
use std::fs;
use std::path::Path;
use tempfile::tempdir;

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

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 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();
        }
    }
}

// ---------------------------------------------------------------------------
// C2 + C5: after compaction + restart, dead entries must not be resurrected
// and dead_bytes counters must be restored correctly.
//
// Strategy: write 200 entries (200 × 32 bytes = 6 400 bytes) across multiple
// files (max_file_size = 4096), overwrite first 100, compact, close, reopen.
// The 100 overwritten entries now have a different value; the old values live
// in the compacted (now removed) files.  If C2 was broken, recovery would
// resurrect the old values from ghost hint entries.
// ---------------------------------------------------------------------------
#[test]
fn test_c2_c5_compaction_dead_entry_not_resurrected() {
    let dir = tempdir().unwrap();
    let mut config = config_1_shard();
    // Each entry is 32 bytes (header 16 + key 8 + value 8, already 8-aligned).
    // 4096 / 32 = 128 entries per file → 200 entries fills ~2 files, guaranteeing
    // at least one immutable file that will be eligible for compaction.
    config.max_file_size = 4096;
    config.write_buffer_size = 4096;
    config.compaction_threshold = 0.1;

    // Phase 1: write 200 entries, overwrite first 100, compact, close.
    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config.clone()).unwrap();

        for i in 0u64..200 {
            tree.put(&i.to_be_bytes(), &(i * 10).to_be_bytes()).unwrap();
        }

        // Overwrite first 100 — this makes the original values dead.
        for i in 0u64..100 {
            tree.put(&i.to_be_bytes(), &(i * 100).to_be_bytes())
                .unwrap();
        }

        tree.compact().unwrap();
        tree.close().unwrap();
    }

    // Phase 2: reopen — recovery must NOT resurrect the old (dead) values.
    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config).unwrap();

        // First 100 keys must have the overwritten value (i * 100).
        for i in 0u64..100 {
            let val = tree.get(&i.to_be_bytes()).unwrap_or_else(|| {
                panic!("key {i} missing after compact+restart");
            });
            assert_eq!(
                val,
                (i * 100).to_be_bytes(),
                "key {i} has wrong value after recovery — dead entry may have been resurrected (C2)"
            );
        }

        // Keys 100..200 must retain their original value (i * 10).
        for i in 100u64..200 {
            let val = tree.get(&i.to_be_bytes()).unwrap_or_else(|| {
                panic!("key {i} missing after compact+restart");
            });
            assert_eq!(
                val,
                (i * 10).to_be_bytes(),
                "key {i} has unexpected value after recovery"
            );
        }

        assert_eq!(
            tree.len(),
            200,
            "expected exactly 200 live entries after compact+restart (C5)"
        );
    }
}

// ---------------------------------------------------------------------------
// C9: a complete entry (fully within the file, not at EOF) with a bad CRC
// must cause recovery to return an error, not silently drop the entry.
//
// We write 2 entries (64 bytes total), close, remove hints so recovery does a
// full scan, then flip a byte in the CRC field of the first entry (bytes 8..11
// of the file).  The first entry is complete and not at EOF, so recovery must
// return DbError::CrcMismatch.
// ---------------------------------------------------------------------------
#[test]
fn test_c9_crc_mismatch_complete_entry_at_eof_is_error() {
    let dir = tempdir().unwrap();
    let config = config_1_shard();

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config.clone()).unwrap();
        // Write two entries so the first one is not at EOF.
        tree.put(&1u64.to_be_bytes(), &2u64.to_be_bytes()).unwrap();
        tree.put(&3u64.to_be_bytes(), &4u64.to_be_bytes()).unwrap();
        tree.close().unwrap();
    }

    let shard_dir = dir.path().join("shard_000");
    remove_hint_files(&shard_dir);

    // Corrupt the CRC of the first entry.
    // EntryHeader layout: gsn(8 bytes) | crc32(4 bytes) | value_len(4 bytes)
    // The first entry starts at offset 0, so CRC bytes are at 8..12.
    let data_files = find_data_files(&shard_dir);
    assert!(!data_files.is_empty(), "no data files found");
    let data_file = &data_files[0];
    let mut data = fs::read(data_file).unwrap();
    assert!(data.len() >= 12, "data file too small to contain CRC field");
    // Flip the CRC byte so both entries are fully within the file but the CRC
    // of the first one is wrong.
    data[8] ^= 0xFF;
    fs::write(data_file, &data).unwrap();

    // Recovery must return an error because a complete entry has a bad CRC.
    let result = ConstTree::<[u8; 8], 8>::open(dir.path(), config);
    assert!(
        result.is_err(),
        "expected CrcMismatch error for corrupt complete entry, but open succeeded"
    );
}

// ---------------------------------------------------------------------------
// C3: corrupting the middle of a data file (after removing hints) must not
// cause a panic.  Recovery may succeed with partial data or return an error,
// but it must not panic or abort.
// ---------------------------------------------------------------------------
#[test]
fn test_c3_corrupted_data_file_does_not_produce_hint() {
    let dir = tempdir().unwrap();
    let mut config = config_1_shard();
    config.max_file_size = 4096;
    config.write_buffer_size = 4096;

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config.clone()).unwrap();
        for i in 0u64..10 {
            tree.put(&i.to_be_bytes(), &(i * 10).to_be_bytes()).unwrap();
        }
        tree.close().unwrap();
    }

    let shard_dir = dir.path().join("shard_000");
    remove_hint_files(&shard_dir);

    // Corrupt the middle of the data file to produce a bad CRC mid-stream.
    let data_files = find_data_files(&shard_dir);
    assert!(!data_files.is_empty(), "no data files found");
    let data_file = &data_files[0];
    let mut data = fs::read(data_file).unwrap();
    if data.len() > 64 {
        // Overwrite 16 bytes in the middle — this lands inside an entry body,
        // making its CRC invalid while leaving the preceding entries intact.
        let mid = data.len() / 2;
        for b in &mut data[mid..mid + 16] {
            *b = 0xFF;
        }
        fs::write(data_file, &data).unwrap();
    }

    // Reopen must not panic.  It may succeed with partial recovery or return
    // an error — both are acceptable as long as no panic/abort occurs.
    let _result = ConstTree::<[u8; 8], 8>::open(dir.path(), config);
}
