use super::*;
use crate::consensus::ConsensusLens;
use stemedb_core::serde::serialize;
use stemedb_core::testing::{test_epoch_with_supersession, AssertionBuilder};
use stemedb_core::types::SupersessionType;
use stemedb_storage::{key_codec, HybridStore};

/// Store an epoch in the KV store and write SUPERSEDED markers.
///
/// This simulates what the IngestWorker does: store the epoch AND write
/// cascade markers for the transitive closure of superseded epochs.
async fn store_epoch(store: &HybridStore, epoch: &Epoch) {
    let key = key_codec::epoch_key(&hex::encode(epoch.id));
    let bytes = serialize(epoch).expect("serialize epoch");
    store.put(&key, &bytes).await.expect("put epoch");

    // Write SUPERSEDED markers for all ancestors in the chain
    if let Some(superseded_id) = epoch.supersedes {
        write_supersession_cascade(store, &epoch.id, &superseded_id).await;
    }
}

/// Write SUPERSEDED markers for the transitive closure (test helper).
///
/// Mirrors the IngestWorker's cascade logic for test setup.
async fn write_supersession_cascade(
    store: &HybridStore,
    new_epoch_id: &[u8; 32],
    superseded_id: &[u8; 32],
) {
    let mut current_id = *superseded_id;
    let mut visited = std::collections::HashSet::new();
    let mut depth = 0;

    loop {
        if !visited.insert(current_id) {
            break; // Cycle
        }
        if depth >= 100 {
            break; // Max depth
        }

        // Write marker
        let marker_key = key_codec::superseded_key(&hex::encode(current_id));
        store.put(&marker_key, new_epoch_id).await.expect("put marker");

        // Check for ancestor
        let epoch_key = key_codec::epoch_key(&hex::encode(current_id));
        let ancestor = match store.get(&epoch_key).await.expect("get") {
            Some(bytes) => stemedb_core::serde::deserialize::<Epoch>(&bytes).ok(),
            None => None,
        };

        match ancestor.and_then(|e| e.supersedes) {
            Some(grandparent_id) => {
                current_id = grandparent_id;
                depth += 1;
            }
            None => break,
        }
    }
}

/// Create a simple epoch without supersession.
fn create_epoch(id: [u8; 32], name: &str) -> Epoch {
    Epoch {
        id,
        name: name.to_string(),
        supersedes: None,
        supersession_type: None,
        start_timestamp: 1000,
        end_timestamp: None,
    }
}

#[tokio::test]
async fn test_empty_candidates() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));
    let lens = EpochAwareLens::with_recency(store);

    let resolution = lens.resolve_async(&[]).await;

    assert!(resolution.winner.is_none());
    assert_eq!(resolution.candidates_count, 0);
}

#[tokio::test]
async fn test_single_candidate() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));
    let lens = EpochAwareLens::with_recency(store);

    let assertion = AssertionBuilder::new().subject("Tesla").timestamp(1000).build();

    let resolution = lens.resolve_async(std::slice::from_ref(&assertion)).await;

    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"Tesla".to_string()));
}

#[tokio::test]
async fn test_epoch_aware_no_epochs_passes_all() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));
    let lens = EpochAwareLens::with_recency(store);

    // Create assertions without epochs
    let a1 = AssertionBuilder::new().subject("A").timestamp(1000).build();
    let a2 = AssertionBuilder::new().subject("B").timestamp(2000).build();

    let resolution = lens.resolve_async(&[a1, a2]).await;

    // RecencyLens picks the newest timestamp
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"B".to_string()));
    assert_eq!(resolution.candidates_count, 2);
}

#[tokio::test]
async fn test_epoch_aware_excludes_superseded() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Create epochs: B supersedes A
    let epoch_a = create_epoch([1u8; 32], "Epoch A");
    let epoch_b = test_epoch_with_supersession(
        [2u8; 32],
        "Epoch B",
        [1u8; 32], // supersedes A
        SupersessionType::Temporal,
    );

    store_epoch(&store, &epoch_a).await;
    store_epoch(&store, &epoch_b).await;

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Create assertions: one in A (should be excluded), one in B (should be included)
    let a_assertion =
        AssertionBuilder::new().subject("InEpochA").epoch([1u8; 32]).timestamp(3000).build();

    let b_assertion =
        AssertionBuilder::new().subject("InEpochB").epoch([2u8; 32]).timestamp(1000).build();

    let resolution = lens.resolve_async(&[a_assertion, b_assertion]).await;

    // Should pick the assertion from epoch B (even though A has higher timestamp)
    // because A is superseded
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"InEpochB".to_string()));
}

#[tokio::test]
async fn test_epoch_aware_chain_supersession() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Create chain: C supersedes B, B supersedes A
    let epoch_a = create_epoch([1u8; 32], "Epoch A");
    let epoch_b = test_epoch_with_supersession(
        [2u8; 32],
        "Epoch B",
        [1u8; 32], // supersedes A
        SupersessionType::Temporal,
    );
    let epoch_c = test_epoch_with_supersession(
        [3u8; 32],
        "Epoch C",
        [2u8; 32], // supersedes B
        SupersessionType::Temporal,
    );

    store_epoch(&store, &epoch_a).await;
    store_epoch(&store, &epoch_b).await;
    store_epoch(&store, &epoch_c).await;

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Create assertions in all three epochs
    let a_assertion =
        AssertionBuilder::new().subject("InEpochA").epoch([1u8; 32]).timestamp(3000).build();

    let b_assertion =
        AssertionBuilder::new().subject("InEpochB").epoch([2u8; 32]).timestamp(2000).build();

    let c_assertion =
        AssertionBuilder::new().subject("InEpochC").epoch([3u8; 32]).timestamp(1000).build();

    let resolution = lens.resolve_async(&[a_assertion, b_assertion, c_assertion]).await;

    // Only epoch C should survive (A and B are superseded)
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"InEpochC".to_string()));
}

#[tokio::test]
async fn test_epoch_aware_missing_epoch_record_includes() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Only store epoch B which supersedes A, but DON'T store epoch A
    let epoch_b = test_epoch_with_supersession(
        [2u8; 32],
        "Epoch B",
        [1u8; 32], // supersedes A (which doesn't exist)
        SupersessionType::Temporal,
    );
    store_epoch(&store, &epoch_b).await;

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Create assertions: one references missing epoch [99], one in B
    let missing_epoch_assertion = AssertionBuilder::new()
        .subject("MissingEpoch")
        .epoch([99u8; 32]) // This epoch doesn't exist in store
        .timestamp(3000)
        .build();

    let b_assertion =
        AssertionBuilder::new().subject("InEpochB").epoch([2u8; 32]).timestamp(1000).build();

    let resolution = lens.resolve_async(&[missing_epoch_assertion, b_assertion]).await;

    // Missing epoch assertion should be included (fail-open)
    // RecencyLens picks the most recent timestamp
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"MissingEpoch".to_string()));
}

#[tokio::test]
async fn test_epoch_aware_cycle_detection() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Create circular supersession: A supersedes B, B supersedes A
    let epoch_a = Epoch {
        id: [1u8; 32],
        name: "Epoch A".to_string(),
        supersedes: Some([2u8; 32]), // A supersedes B
        supersession_type: Some(SupersessionType::Temporal),
        start_timestamp: 1000,
        end_timestamp: None,
    };
    let epoch_b = Epoch {
        id: [2u8; 32],
        name: "Epoch B".to_string(),
        supersedes: Some([1u8; 32]), // B supersedes A (cycle!)
        supersession_type: Some(SupersessionType::Temporal),
        start_timestamp: 1000,
        end_timestamp: None,
    };

    store_epoch(&store, &epoch_a).await;
    store_epoch(&store, &epoch_b).await;

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Create assertions in both epochs
    let a_assertion =
        AssertionBuilder::new().subject("InEpochA").epoch([1u8; 32]).timestamp(2000).build();

    let b_assertion =
        AssertionBuilder::new().subject("InEpochB").epoch([2u8; 32]).timestamp(1000).build();

    // Should not hang or panic - cycle detection should kick in.
    // If cycle detection fails, this would hang forever (test timeout would catch it).
    let resolution = lens.resolve_async(&[a_assertion, b_assertion]).await;

    // In a mutual supersession cycle (A supersedes B, B supersedes A):
    // - Walking from A: A.supersedes = B, so B is added to superseded set
    // - Walking from B: B.supersedes = A, so A is added to superseded set
    // - Both epochs end up in the superseded set
    // - All candidates are filtered out
    // - Result is empty resolution
    //
    // This is the correct behavior for pathological data: mutual supersession
    // means "both are obsolete" which results in no valid candidates.
    assert!(resolution.winner.is_none(), "mutual supersession should filter all candidates");
    assert_eq!(resolution.candidates_count, 0, "empty resolution has 0 candidates_count");
}

#[tokio::test]
async fn test_epoch_aware_with_consensus_lens() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Create epochs: B supersedes A
    let epoch_a = create_epoch([1u8; 32], "Epoch A");
    let epoch_b =
        test_epoch_with_supersession([2u8; 32], "Epoch B", [1u8; 32], SupersessionType::Temporal);

    store_epoch(&store, &epoch_a).await;
    store_epoch(&store, &epoch_b).await;

    // Use ConsensusLens as inner lens
    let lens = EpochAwareLens::with_sync_lens(Arc::clone(&store), ConsensusLens);

    // Create multiple assertions with same value in epoch B (consensus)
    let b1 = AssertionBuilder::new()
        .subject("B1")
        .object_number(100.0)
        .epoch([2u8; 32])
        .timestamp(1000)
        .build();

    let b2 = AssertionBuilder::new()
        .subject("B2")
        .object_number(100.0)
        .epoch([2u8; 32])
        .timestamp(2000)
        .build();

    // Assertion in superseded epoch A
    let a1 = AssertionBuilder::new()
        .subject("A1")
        .object_number(200.0)
        .epoch([1u8; 32])
        .timestamp(3000)
        .build();

    let resolution = lens.resolve_async(&[b1, b2, a1]).await;

    // Should exclude A1 (superseded epoch), then ConsensusLens picks from B1, B2
    assert!(resolution.winner.is_some());
    // ConsensusLens groups by object value - both B1 and B2 have 100.0
    // The winner should be from epoch B
    let winner = resolution.winner.as_ref().expect("winner");
    assert!(winner.epoch == Some([2u8; 32]));
}

#[tokio::test]
async fn test_epoch_aware_mixed_epochs_and_no_epochs() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Create epochs: B supersedes A
    let epoch_a = create_epoch([1u8; 32], "Epoch A");
    let epoch_b =
        test_epoch_with_supersession([2u8; 32], "Epoch B", [1u8; 32], SupersessionType::Temporal);

    store_epoch(&store, &epoch_a).await;
    store_epoch(&store, &epoch_b).await;

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Mix of assertions: some with epochs, some without
    let no_epoch = AssertionBuilder::new().subject("NoEpoch").timestamp(1000).build();

    let in_a = AssertionBuilder::new().subject("InA").epoch([1u8; 32]).timestamp(3000).build();

    let in_b = AssertionBuilder::new().subject("InB").epoch([2u8; 32]).timestamp(2000).build();

    let resolution = lens.resolve_async(&[no_epoch, in_a, in_b]).await;

    // NoEpoch should pass (no epoch = always included)
    // InA should be excluded (superseded)
    // InB should pass
    // RecencyLens picks InB (timestamp 2000) over NoEpoch (timestamp 1000)
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"InB".to_string()));
}

#[tokio::test]
async fn test_superseded_epoch_filtered_even_without_new_assertions() {
    // With the O(1) marker-based approach, epochs are filtered based on
    // SUPERSEDED: markers, not based on what's in the candidate set.
    // If an epoch has a SUPERSEDED marker, its assertions are filtered.
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Create epochs: B supersedes A
    let epoch_a = create_epoch([1u8; 32], "Epoch A");
    let epoch_b =
        test_epoch_with_supersession([2u8; 32], "Epoch B", [1u8; 32], SupersessionType::Temporal);

    // Store both epochs - this writes the SUPERSEDED:A marker
    store_epoch(&store, &epoch_a).await;
    store_epoch(&store, &epoch_b).await;

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Only assertions from "old" epoch A (no epoch B assertions in candidates)
    // With the marker-based approach, A is superseded regardless of candidate set
    let a1 = AssertionBuilder::new().subject("A1").epoch([1u8; 32]).timestamp(1000).build();
    let a2 = AssertionBuilder::new().subject("A2").epoch([1u8; 32]).timestamp(2000).build();

    let resolution = lens.resolve_async(&[a1, a2]).await;

    // Both assertions are from superseded epoch A, so both are filtered out
    // Result is empty resolution
    assert!(resolution.winner.is_none(), "All assertions from superseded epoch should be filtered");
    assert_eq!(resolution.candidates_count, 0);
}

#[tokio::test]
async fn test_epoch_without_marker_passes_through() {
    // This test documents fail-open behavior:
    // If an epoch doesn't have a SUPERSEDED marker (e.g., data from before
    // cascade logic was added), assertions pass through.
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Manually store epoch A WITHOUT writing cascade markers
    // (simulating old data before the cascade feature)
    let epoch_a = create_epoch([1u8; 32], "Epoch A");
    let key = key_codec::epoch_key(&hex::encode(epoch_a.id));
    let bytes = serialize(&epoch_a).expect("serialize epoch");
    store.put(&key, &bytes).await.expect("put epoch");

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    let a1 = AssertionBuilder::new().subject("A1").epoch([1u8; 32]).timestamp(1000).build();
    let a2 = AssertionBuilder::new().subject("A2").epoch([1u8; 32]).timestamp(2000).build();

    let resolution = lens.resolve_async(&[a1, a2]).await;

    // No SUPERSEDED marker for A, so assertions pass through (fail-open)
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"A2".to_string()));
}

#[tokio::test]
async fn test_lens_name() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));
    let lens = EpochAwareLens::with_recency(store);

    assert_eq!(lens.name(), "EpochAware");
}

/// Test: O(1) marker lookup works without reading epoch records.
///
/// This verifies the optimization: with SUPERSEDED markers present,
/// we don't need to read E:{epoch_id} records to determine supersession.
#[tokio::test]
async fn test_epoch_aware_uses_marker_not_epoch_record() {
    let store = Arc::new(HybridStore::open_temp().expect("store"));

    // Write ONLY the SUPERSEDED marker, NOT the epoch records themselves
    // This tests that we use the marker for filtering, not the epoch record
    let marker_key = key_codec::superseded_key(&hex::encode([1u8; 32]));
    store.put(&marker_key, &[2u8; 32]).await.expect("put marker");

    let lens = EpochAwareLens::with_recency(Arc::clone(&store));

    // Create assertions: one in "superseded" epoch A, one in epoch B
    let a_assertion =
        AssertionBuilder::new().subject("InEpochA").epoch([1u8; 32]).timestamp(3000).build();
    let b_assertion =
        AssertionBuilder::new().subject("InEpochB").epoch([2u8; 32]).timestamp(1000).build();

    let resolution = lens.resolve_async(&[a_assertion, b_assertion]).await;

    // Epoch A is superseded (marker exists), so InEpochA is filtered
    // Only InEpochB survives
    assert!(resolution.winner.is_some());
    assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"InEpochB".to_string()));

    // Verify we didn't need to read E:{epoch_id} records at all
    // (they don't exist in this test)
    let epochs = store.scan_prefix(b"\x00E:").await.expect("scan");
    assert_eq!(epochs.len(), 0, "No epoch records should exist - test uses marker only");
}
