//! Unit tests for MemTable.

use std::time::Duration;

use stemedb_core::types::{Assertion, HlcTimestamp, LifecycleStage, ObjectValue, SourceClass};

use super::{MemTable, MemTableEntry};

fn make_test_assertion(subject: &str, predicate: &str) -> Assertion {
    Assertion {
        subject: subject.to_string(),
        predicate: predicate.to_string(),
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        narrative: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![],
        confidence: 0.9,
        timestamp: 1234567890,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    }
}

#[test]
fn test_insert_and_get_by_hash() {
    let memtable = MemTable::new(100);
    let assertion = make_test_assertion("Tesla", "revenue");
    let hash = [1u8; 32];
    let entry = MemTableEntry::new(assertion.clone(), hash, 100);

    memtable.insert(entry);

    let result = memtable.get_by_hash(&hash);
    assert!(result.is_some());
    assert_eq!(result.as_ref().map(|a| &a.subject), Some(&"Tesla".to_string()));
    assert_eq!(memtable.len(), 1);
}

#[test]
fn test_insert_and_get_by_subject() {
    let memtable = MemTable::new(100);

    let assertion1 = make_test_assertion("Tesla", "revenue");
    let assertion2 = make_test_assertion("Tesla", "profit");

    memtable.insert(MemTableEntry::new(assertion1, [1u8; 32], 100));
    memtable.insert(MemTableEntry::new(assertion2, [2u8; 32], 200));

    let results = memtable.get_by_subject("Tesla");
    assert_eq!(results.len(), 2);

    let predicates: Vec<_> = results.iter().map(|a| &a.predicate).collect();
    assert!(predicates.contains(&&"revenue".to_string()));
    assert!(predicates.contains(&&"profit".to_string()));
}

#[test]
fn test_insert_and_get_by_subject_predicate() {
    let memtable = MemTable::new(100);

    let assertion1 = make_test_assertion("Tesla", "revenue");
    let assertion2 = make_test_assertion("Tesla", "revenue");
    let assertion3 = make_test_assertion("Tesla", "profit");

    memtable.insert(MemTableEntry::new(assertion1, [1u8; 32], 100));
    memtable.insert(MemTableEntry::new(assertion2, [2u8; 32], 200));
    memtable.insert(MemTableEntry::new(assertion3, [3u8; 32], 300));

    let results = memtable.get_by_subject_predicate("Tesla", "revenue");
    assert_eq!(results.len(), 2);

    let results = memtable.get_by_subject_predicate("Tesla", "profit");
    assert_eq!(results.len(), 1);
}

#[test]
fn test_eviction_by_offset() {
    let memtable = MemTable::new(100);

    let assertion1 = make_test_assertion("Tesla", "revenue");
    let assertion2 = make_test_assertion("Apple", "revenue");

    memtable.insert(MemTableEntry::new(assertion1, [1u8; 32], 100));
    memtable.insert(MemTableEntry::new(assertion2, [2u8; 32], 200));

    assert_eq!(memtable.len(), 2);

    // Advance indexed offset to 150 - should evict first entry
    memtable.advance_indexed_offset(150);

    assert_eq!(memtable.len(), 1);
    assert!(memtable.get_by_hash(&[1u8; 32]).is_none());
    assert!(memtable.get_by_hash(&[2u8; 32]).is_some());

    // Advance to 250 - should evict second entry
    memtable.advance_indexed_offset(250);

    assert_eq!(memtable.len(), 0);
}

#[test]
fn test_eviction_updates_subject_index() {
    let memtable = MemTable::new(100);

    let assertion1 = make_test_assertion("Tesla", "revenue");
    let assertion2 = make_test_assertion("Tesla", "profit");

    memtable.insert(MemTableEntry::new(assertion1, [1u8; 32], 100));
    memtable.insert(MemTableEntry::new(assertion2, [2u8; 32], 200));

    assert_eq!(memtable.get_by_subject("Tesla").len(), 2);

    // Evict first entry
    memtable.advance_indexed_offset(150);

    let results = memtable.get_by_subject("Tesla");
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].predicate, "profit");
}

#[test]
fn test_idempotent_insert() {
    let memtable = MemTable::new(100);

    let assertion = make_test_assertion("Tesla", "revenue");
    let hash = [1u8; 32];

    memtable.insert(MemTableEntry::new(assertion.clone(), hash, 100));
    memtable.insert(MemTableEntry::new(assertion.clone(), hash, 100));
    memtable.insert(MemTableEntry::new(assertion, hash, 100));

    // Should only have 1 entry
    assert_eq!(memtable.len(), 1);
    assert_eq!(memtable.get_by_subject("Tesla").len(), 1);
}

#[test]
fn test_empty_lookups() {
    let memtable = MemTable::new(100);

    assert!(memtable.get_by_hash(&[0u8; 32]).is_none());
    assert!(memtable.get_by_subject("Nonexistent").is_empty());
    assert!(memtable.get_by_subject_predicate("Nonexistent", "pred").is_empty());
}

#[test]
fn test_multiple_subjects_isolated() {
    let memtable = MemTable::new(100);

    let assertion1 = make_test_assertion("Tesla", "revenue");
    let assertion2 = make_test_assertion("Apple", "revenue");

    memtable.insert(MemTableEntry::new(assertion1, [1u8; 32], 100));
    memtable.insert(MemTableEntry::new(assertion2, [2u8; 32], 200));

    assert_eq!(memtable.get_by_subject("Tesla").len(), 1);
    assert_eq!(memtable.get_by_subject("Apple").len(), 1);
    assert_eq!(memtable.get_by_subject("Tesla")[0].subject, "Tesla");
    assert_eq!(memtable.get_by_subject("Apple")[0].subject, "Apple");
}

#[tokio::test]
async fn test_concurrent_insert_and_read() {
    use std::sync::Arc;

    let memtable = Arc::new(MemTable::new(10_000));

    // Spawn writers
    let mut handles = Vec::new();
    for i in 0..10 {
        let mt = Arc::clone(&memtable);
        handles.push(tokio::spawn(async move {
            for j in 0..100 {
                let subject = format!("Subject_{}", i);
                let assertion = make_test_assertion(&subject, "predicate");
                let mut hash = [0u8; 32];
                hash[0] = i as u8;
                hash[1] = j as u8;
                mt.insert(MemTableEntry::new(assertion, hash, (i * 100 + j) as u64));
            }
        }));
    }

    // Spawn readers concurrently
    for i in 0..10 {
        let mt = Arc::clone(&memtable);
        handles.push(tokio::spawn(async move {
            for _ in 0..50 {
                let _ = mt.get_by_subject(&format!("Subject_{}", i));
                tokio::task::yield_now().await;
            }
        }));
    }

    for handle in handles {
        handle.await.unwrap();
    }

    // Should have 1000 entries total (10 writers * 100 entries)
    assert_eq!(memtable.len(), 1000);
}

#[test]
fn test_indexed_offset_tracking() {
    let memtable = MemTable::new(100);

    assert_eq!(memtable.indexed_offset(), 0);

    memtable.advance_indexed_offset(100);
    assert_eq!(memtable.indexed_offset(), 100);

    memtable.advance_indexed_offset(50); // Should not go backwards
    assert_eq!(memtable.indexed_offset(), 100);

    memtable.advance_indexed_offset(200);
    assert_eq!(memtable.indexed_offset(), 200);
}

#[test]
fn test_stale_eviction_with_short_max_age() {
    // Use a very short max age for testing
    let memtable = MemTable::with_max_age(2, Duration::from_millis(10));

    let assertion1 = make_test_assertion("Tesla", "revenue");
    let assertion2 = make_test_assertion("Apple", "revenue");

    memtable.insert(MemTableEntry::new(assertion1, [1u8; 32], 100));

    // Wait for the entry to become stale
    std::thread::sleep(Duration::from_millis(20));

    // Insert another entry to trigger eviction
    memtable.insert(MemTableEntry::new(assertion2, [2u8; 32], 200));

    // The first entry should have been evicted due to age, but the second is new
    // After the third insert (triggering eviction due to max_entries=2), only the newest remains
    let assertion3 = make_test_assertion("Google", "revenue");
    memtable.insert(MemTableEntry::new(assertion3, [3u8; 32], 300));

    // First entry should be gone (stale)
    assert!(memtable.get_by_hash(&[1u8; 32]).is_none());
}
