//! m7p3 social scale correctness tests.
//!
//! Validates the CoEngagementIndex eviction invariant at 2× capacity and
//! the social graph memory bound at production scale.

#![allow(clippy::unwrap_used, clippy::cast_precision_loss)]

use tidaldb::entities::CoEngagementIndex;
use tidaldb::schema::EntityId;

// ── CoEngagementIndex eviction correctness ────────────────────────────────────

/// Insert 2× capacity edges; verify edge_count <= capacity invariant always holds.
///
/// This is the core memory-bound invariant: no matter how many edges are added,
/// the index never grows beyond `capacity`.
#[test]
fn eviction_correctness_at_2x_capacity() {
    let capacity = 500;
    let index = CoEngagementIndex::with_capacity(capacity);

    // Insert 2×capacity items for a single user — each step may trigger eviction.
    for i in 1u64..=(2 * capacity as u64) {
        index.record_positive(1, EntityId::new(i));
        assert!(
            index.edge_count() <= capacity,
            "edge_count={} exceeded capacity={capacity} at step i={i}",
            index.edge_count()
        );
    }

    assert!(
        index.edge_count() <= capacity,
        "final edge_count={} must be <= capacity={capacity}",
        index.edge_count()
    );
}

/// High-weight edges must survive when low-weight edges fill to 2× capacity.
///
/// Inserts one very high-weight edge (weight=100.0) then floods with low-weight
/// edges until we exceed 2× capacity. The high-weight edge must still be present.
#[test]
fn high_weight_edge_survival_under_eviction() {
    let capacity = 100;
    let index = CoEngagementIndex::with_capacity(capacity);

    // Insert one very strong edge directly.
    let anchor_item_a = 99_999u64;
    let anchor_item_b = 88_888u64;
    index.insert_edge(anchor_item_a, anchor_item_b, 100.0);

    // Now flood with 2× capacity low-weight record_positive calls.
    // Each record_positive adds up to USER_RECENT_CAPACITY new edges.
    for i in 1u64..=(2 * capacity as u64) {
        index.record_positive(1, EntityId::new(i));
    }

    // The capacity invariant must hold.
    assert!(
        index.edge_count() <= capacity,
        "edge_count={} exceeds capacity={capacity}",
        index.edge_count()
    );

    // The high-weight anchor edge must have survived.
    let anchor_score = index.score(EntityId::new(anchor_item_a), EntityId::new(anchor_item_b));
    assert!(
        anchor_score > 50.0,
        "anchor edge (weight=100) was evicted: score={anchor_score}"
    );
}

/// Max observed edge_count must never exceed capacity across any sequence of insertions.
///
/// Uses multiple users and varied items to stress the eviction path at
/// 2× capacity.
#[test]
fn co_engagement_memory_bounded_at_2x_insertions() {
    let capacity = 200;
    let index = CoEngagementIndex::with_capacity(capacity);

    // 10 users, each engaging with 40 items → many cross-user edges.
    for user_id in 1u64..=10 {
        for item_id in 1u64..=40 {
            let item = (user_id - 1) * 40 + item_id;
            index.record_positive(user_id, EntityId::new(item));
            assert!(
                index.edge_count() <= capacity,
                "capacity invariant violated at user={user_id} item={item_id}: \
                 edge_count={} > capacity={capacity}",
                index.edge_count()
            );
        }
    }
}

/// Eviction does not cause self-loops under adversarial access patterns.
///
/// Alternating engagement with two items should never create (a, a) or (b, b) edges.
#[test]
fn no_self_loops_after_eviction() {
    let capacity = 5;
    let index = CoEngagementIndex::with_capacity(capacity);

    for i in 0..50u64 {
        let item = if i % 2 == 0 { 1 } else { 2 };
        index.record_positive(1, EntityId::new(item));
    }

    assert_eq!(
        index.score(EntityId::new(1), EntityId::new(1)),
        0.0,
        "self-loop (1, 1) created"
    );
    assert_eq!(
        index.score(EntityId::new(2), EntityId::new(2)),
        0.0,
        "self-loop (2, 2) created"
    );
}

/// After eviction, the index correctly reports top candidates.
///
/// After eviction the remaining edges should be consistent and queryable.
#[test]
fn top_candidates_consistent_after_eviction() {
    let capacity = 10;
    let index = CoEngagementIndex::with_capacity(capacity);

    // User engages with 30 items, which will trigger eviction.
    for i in 1u64..=30 {
        index.record_positive(1, EntityId::new(i));
    }

    assert!(
        index.edge_count() <= capacity,
        "edge_count={} > capacity={capacity}",
        index.edge_count()
    );

    // top_candidates should return a consistent subset.
    // For seed item 30 (last inserted), top candidates should be non-empty.
    // We can't assert exact values after eviction, but the operation must succeed.
    let candidates = index.top_candidates(EntityId::new(30), 5);
    for (id, weight) in &candidates {
        assert!(id.as_u64() > 0, "invalid candidate id=0");
        assert!(*weight > 0.0, "zero-weight candidate");
    }
}

// ── Property test: edge_count never exceeds capacity ──────────────────────────

mod proptests {
    use proptest::prelude::*;
    use tidaldb::entities::CoEngagementIndex;
    use tidaldb::schema::EntityId;

    proptest! {
        /// The CoEngagementIndex memory bound invariant must hold for any sequence
        /// of record_positive calls, regardless of capacity, user count, or item count.
        #[test]
        fn edge_count_never_exceeds_capacity(
            capacity in 5usize..=200,
            users in 1u64..=10,
            items_per_user in 1u64..=30,
        ) {
            let index = CoEngagementIndex::with_capacity(capacity);

            for user_id in 1..=users {
                for item_idx in 0..items_per_user {
                    let item_id = user_id * 1000 + item_idx;
                    index.record_positive(user_id, EntityId::new(item_id));

                    prop_assert!(
                        index.edge_count() <= capacity,
                        "edge_count={} exceeded capacity={} at user={} item_idx={}",
                        index.edge_count(),
                        capacity,
                        user_id,
                        item_idx,
                    );
                }
            }

            // Final invariant
            prop_assert!(
                index.edge_count() <= capacity,
                "final edge_count={} exceeds capacity={}",
                index.edge_count(),
                capacity,
            );
        }
    }
}
