use std::sync::Arc;
use std::thread;

use ontoref_blobs::{BlobId, BlobStore, Error};

#[test]
fn put_get_round_trip() {
    let dir = tempfile::tempdir().unwrap();
    let store = BlobStore::open(dir.path()).unwrap();
    let id = store.put(b"hello world").unwrap();
    assert_eq!(store.get(&id).unwrap(), b"hello world");
}

#[test]
fn put_get_empty_bytes() {
    let dir = tempfile::tempdir().unwrap();
    let store = BlobStore::open(dir.path()).unwrap();
    let id = store.put(b"").unwrap();
    assert_eq!(store.get(&id).unwrap(), Vec::<u8>::new());
}

#[test]
fn put_get_is_idempotent() {
    let dir = tempfile::tempdir().unwrap();
    let store = BlobStore::open(dir.path()).unwrap();
    let id1 = store.put(b"same content").unwrap();
    let id2 = store.put(b"same content").unwrap();
    assert_eq!(id1, id2);
    assert_eq!(store.get(&id1).unwrap(), b"same content");
}

#[test]
fn put_get_missing_returns_not_found() {
    let dir = tempfile::tempdir().unwrap();
    let store = BlobStore::open(dir.path()).unwrap();
    let dummy = BlobId([0u8; 32]);
    match store.get(&dummy) {
        Err(Error::NotFound(_)) => {}
        other => panic!("expected NotFound, got {other:?}"),
    }
}

#[test]
fn concurrent_puts_of_same_blob() {
    let dir = tempfile::tempdir().unwrap();
    let store = Arc::new(BlobStore::open(dir.path()).unwrap());
    let payload = b"shared content".to_vec();

    let handles: Vec<_> = (0..16)
        .map(|_| {
            let store = Arc::clone(&store);
            let payload = payload.clone();
            thread::spawn(move || store.put(&payload).unwrap())
        })
        .collect();

    let ids: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
    let first = ids[0];
    for id in &ids {
        assert_eq!(*id, first);
    }
    assert_eq!(store.get(&first).unwrap(), payload);
}

#[test]
fn concurrent_puts_of_different_blobs() {
    let dir = tempfile::tempdir().unwrap();
    let store = Arc::new(BlobStore::open(dir.path()).unwrap());

    let handles: Vec<_> = (0..32)
        .map(|i| {
            let store = Arc::clone(&store);
            let payload = format!("blob #{i}").into_bytes();
            thread::spawn(move || {
                let id = store.put(&payload).unwrap();
                (id, payload)
            })
        })
        .collect();

    for handle in handles {
        let (id, payload) = handle.join().unwrap();
        assert_eq!(store.get(&id).unwrap(), payload);
    }
}

#[test]
fn crash_safe_no_orphan_tmp_after_successful_put() {
    let dir = tempfile::tempdir().unwrap();
    let store = BlobStore::open(dir.path()).unwrap();
    let id = store.put(b"normal content").unwrap();
    let parent = store.path_for(&id).parent().unwrap().to_path_buf();

    let entries: Vec<String> = std::fs::read_dir(&parent)
        .unwrap()
        .filter_map(|entry| entry.ok())
        .map(|entry| entry.file_name().to_string_lossy().into_owned())
        .collect();

    assert_eq!(
        entries,
        vec![id.to_hex()],
        "blob directory should contain exactly the finalised blob"
    );
}

#[test]
fn crash_safe_stale_tmp_does_not_break_get() {
    let dir = tempfile::tempdir().unwrap();
    let store = BlobStore::open(dir.path()).unwrap();
    let id = store.put(b"real").unwrap();

    let parent = store.path_for(&id).parent().unwrap().to_path_buf();
    std::fs::write(parent.join(".tmp.garbage"), b"garbage from a crashed put").unwrap();

    assert_eq!(store.get(&id).unwrap(), b"real");
}
