//! Periodic-flush behaviour: light `periodic_flush()` semantics and the
//! `Db` background flusher. Tests are designed so teardown flush
//! (`Db::drop` -> `shutdown` -> `close`) cannot satisfy the assertions —
//! the writer `Db` stays alive until after every assertion.
#![cfg(all(feature = "typed-tree", feature = "armour"))]

use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use armdb::armour::{Collection, Db};
use armdb::{CollectionMeta, Config, NoHook};
use armour_core::GetType;

#[derive(
    Clone,
    Copy,
    Debug,
    PartialEq,
    GetType,
    zerocopy::FromBytes,
    zerocopy::IntoBytes,
    zerocopy::Immutable,
)]
#[repr(C)]
struct V8(u64);

impl CollectionMeta for V8 {
    type SelfId = [u8; 8];
    const NAME: &'static str = "pf_items";
    const VERSION: u16 = 1;
}

fn k(n: u64) -> [u8; 8] {
    n.to_be_bytes()
}

fn collect_ext(dir: &Path, ext: &str, out: &mut Vec<PathBuf>) {
    if let Ok(rd) = std::fs::read_dir(dir) {
        for e in rd.flatten() {
            let p = e.path();
            if p.is_dir() {
                collect_ext(&p, ext, out);
            } else if p.extension().and_then(|s| s.to_str()) == Some(ext) {
                out.push(p);
            }
        }
    }
}
fn count_ext(dir: &Path, ext: &str) -> usize {
    let mut v = Vec::new();
    collect_ext(dir, ext, &mut v);
    v.len()
}
fn size_ext(dir: &Path, ext: &str) -> u64 {
    let mut v = Vec::new();
    collect_ext(dir, ext, &mut v);
    v.iter()
        .filter_map(|p| std::fs::metadata(p).ok())
        .map(|m| m.len())
        .sum()
}

#[test]
fn periodic_flush_is_light_no_hint_rebuild() {
    let dir = tempfile::tempdir().unwrap();
    // open_test => no background flusher. Config::test() has hints=true so the
    // explicit hint path writes a .hint file; periodic_flush() must not.
    let db = Db::open_test(dir.path()).unwrap();
    let tree = db
        .open_zero_tree::<V8, 8, _>(Config::test(), NoHook, &[])
        .unwrap();

    tree.put(&k(1), &V8(42)).unwrap();

    // periodic_flush: userspace buffer -> page cache, NO hint rebuild.
    tree.periodic_flush().unwrap();
    assert!(
        size_ext(dir.path(), "data") > 0,
        "periodic_flush did not write buffered data to the .data file"
    );
    assert_eq!(
        count_ext(dir.path(), "hint"),
        0,
        "periodic_flush must not rebuild hint files"
    );

    // The explicit hint path (sync_hints) DOES write hint files — proving
    // periodic_flush deliberately skipped that work.
    //
    // NOTE: a concrete `ZeroTree` handle has an *inherent* `flush()`
    // (= engine flush + fsync, no hints) that shadows the trait method, so we
    // call the unambiguous inherent `sync_hints()` here rather than `flush()`.
    // The `Db` flush/close path is unaffected — it goes through `dyn Collection`
    // and calls the trait methods.
    tree.sync_hints().unwrap();
    assert!(
        count_ext(dir.path(), "hint") > 0,
        "sync_hints must write hint files"
    );

    drop(tree);
    drop(db);
}

#[test]
fn db_periodic_flush_writes_data_while_alive() {
    let dir = tempfile::tempdir().unwrap();
    // Short flush interval, compaction off. The writer Db stays alive through
    // the assertion, so any on-disk data growth is from the periodic tick,
    // NOT teardown flush.
    let db = Db::open_with_intervals(dir.path(), None, Some(Duration::from_millis(50))).unwrap();
    let tree = db
        .open_zero_tree::<V8, 8, _>(Config::test(), NoHook, &[])
        .unwrap();

    let before = size_ext(dir.path(), "data");
    tree.put(&k(1), &V8(7)).unwrap();
    // Deliberately no flush()/close() here.

    let deadline = Instant::now() + Duration::from_secs(5);
    let mut grew = false;
    while Instant::now() < deadline {
        if size_ext(dir.path(), "data") > before {
            grew = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(10));
    }
    assert!(
        grew,
        "periodic flusher did not write buffered data to disk while Db alive"
    );

    // Keep db + tree alive until here so teardown cannot be credited.
    drop(tree);
    drop(db);
}

#[test]
fn db_no_flusher_keeps_data_buffered() {
    let dir = tempfile::tempdir().unwrap();
    // flush_interval = None => no background flusher (the open_test default).
    let db = Db::open_with_intervals(dir.path(), None, None).unwrap();
    let tree = db
        .open_zero_tree::<V8, 8, _>(Config::test(), NoHook, &[])
        .unwrap();

    let before = size_ext(dir.path(), "data");
    tree.put(&k(1), &V8(7)).unwrap();

    // Without a flusher and while the writer is alive, the small put stays in
    // the userspace buffer — on-disk .data does not grow.
    std::thread::sleep(Duration::from_millis(300));
    assert_eq!(
        size_ext(dir.path(), "data"),
        before,
        "data grew on disk without a flusher and before teardown"
    );

    drop(tree);
    drop(db);
}
