use super::*;

// ── ShardId / RegionId tests (existing) ────────────────────────────────

#[test]
fn shard_id_single_is_zero() {
    assert_eq!(ShardId::SINGLE, ShardId(0));
}

#[test]
fn region_id_single_is_zero() {
    assert_eq!(RegionId::SINGLE, RegionId(0));
}

#[test]
fn shard_id_display() {
    assert_eq!(ShardId(3).to_string(), "s3");
    assert_eq!(ShardId(0).to_string(), "s0");
}

#[test]
fn region_id_display() {
    assert_eq!(RegionId(2).to_string(), "r2");
    assert_eq!(RegionId(0).to_string(), "r0");
}

#[test]
fn shard_id_ordering() {
    assert!(ShardId(0) < ShardId(1));
    assert!(ShardId(1) < ShardId(100));
}

#[test]
fn region_id_ordering() {
    assert!(RegionId(0) < RegionId(1));
}

// ── EntityIdRange tests ────────────────────────────────────────────────

#[test]
fn entity_id_range_contains_basic() {
    let range = EntityIdRange { start: 10, end: 20 };
    assert!(!range.contains(9));
    assert!(range.contains(10));
    assert!(range.contains(15));
    assert!(range.contains(19));
    assert!(!range.contains(20));
}

#[test]
fn entity_id_range_contains_sentinel_end() {
    // end = u64::MAX means "includes everything from start onward"
    let range = EntityIdRange {
        start: 100,
        end: u64::MAX,
    };
    assert!(!range.contains(99));
    assert!(range.contains(100));
    assert!(range.contains(u64::MAX - 1));
    assert!(range.contains(u64::MAX)); // sentinel: inclusive of MAX
}

#[test]
fn entity_id_range_full_covers_everything() {
    let full = EntityIdRange::full();
    assert!(full.contains(0));
    assert!(full.contains(u64::MAX / 2));
    assert!(full.contains(u64::MAX));
}

// ── ShardRouter: Single ────────────────────────────────────────────────

#[test]
fn single_router_always_returns_shard_zero() {
    let router = ShardRouter::single();
    assert_eq!(router.route(EntityId::new(0)), ShardId::SINGLE);
    assert_eq!(router.route(EntityId::new(42)), ShardId::SINGLE);
    assert_eq!(router.route(EntityId::new(u64::MAX)), ShardId::SINGLE);
}

#[test]
fn single_router_all_shards() {
    let router = ShardRouter::single();
    assert_eq!(router.all_shards(), vec![ShardId::SINGLE]);
}

// ── ShardRouter: Hash ──────────────────────────────────────────────────

#[test]
fn hash_router_zero_shards_is_error() {
    let err = ShardRouter::hash(0).unwrap_err();
    assert!(err.to_string().contains("must be > 0"));
}

#[test]
fn hash_routing_is_deterministic_spot_check() {
    let router = ShardRouter::hash(5).unwrap();
    for id in [0u64, 1, 42, 1000, u64::MAX] {
        let entity = EntityId::new(id);
        assert_eq!(router.route(entity), router.route(entity));
    }
}

#[test]
fn hash_routing_stays_in_range_spot_check() {
    let router = ShardRouter::hash(7).unwrap();
    for id in [0u64, 1, 100, 10_000, u64::MAX - 1, u64::MAX] {
        let shard = router.route(EntityId::new(id));
        assert!(shard.0 < 7, "shard {} out of range for id {}", shard, id);
    }
}

#[test]
fn hash_router_all_shards() {
    let router = ShardRouter::hash(4).unwrap();
    let shards = router.all_shards();
    assert_eq!(shards, vec![ShardId(0), ShardId(1), ShardId(2), ShardId(3)]);
}

#[test]
fn hash_router_single_shard_always_zero() {
    let router = ShardRouter::hash(1).unwrap();
    for id in [0u64, 42, u64::MAX] {
        assert_eq!(router.route(EntityId::new(id)), ShardId(0));
    }
}

// ── ShardRouter: Range ─────────────────────────────────────────────────

#[test]
fn range_router_correct_routing() {
    let ranges = vec![
        (ShardId(0), EntityIdRange { start: 0, end: 100 }),
        (
            ShardId(1),
            EntityIdRange {
                start: 100,
                end: u64::MAX,
            },
        ),
    ];
    let router = ShardRouter::range(ranges).unwrap();

    assert_eq!(router.route(EntityId::new(0)), ShardId(0));
    assert_eq!(router.route(EntityId::new(50)), ShardId(0));
    assert_eq!(router.route(EntityId::new(99)), ShardId(0));
    assert_eq!(router.route(EntityId::new(100)), ShardId(1));
    assert_eq!(router.route(EntityId::new(u64::MAX)), ShardId(1));
}

#[test]
fn range_router_three_shards() {
    let ranges = vec![
        (
            ShardId(0),
            EntityIdRange {
                start: 0,
                end: 1000,
            },
        ),
        (
            ShardId(1),
            EntityIdRange {
                start: 1000,
                end: 2000,
            },
        ),
        (
            ShardId(2),
            EntityIdRange {
                start: 2000,
                end: u64::MAX,
            },
        ),
    ];
    let router = ShardRouter::range(ranges).unwrap();

    assert_eq!(router.route(EntityId::new(0)), ShardId(0));
    assert_eq!(router.route(EntityId::new(999)), ShardId(0));
    assert_eq!(router.route(EntityId::new(1000)), ShardId(1));
    assert_eq!(router.route(EntityId::new(1999)), ShardId(1));
    assert_eq!(router.route(EntityId::new(2000)), ShardId(2));
    assert_eq!(router.route(EntityId::new(u64::MAX)), ShardId(2));
}

#[test]
fn range_router_unsorted_input_is_sorted() {
    // Pass ranges out of order -- validate_ranges sorts them.
    let ranges = vec![
        (
            ShardId(1),
            EntityIdRange {
                start: 500,
                end: u64::MAX,
            },
        ),
        (ShardId(0), EntityIdRange { start: 0, end: 500 }),
    ];
    let router = ShardRouter::range(ranges).unwrap();
    assert_eq!(router.route(EntityId::new(0)), ShardId(0));
    assert_eq!(router.route(EntityId::new(500)), ShardId(1));
}

#[test]
fn range_router_all_shards() {
    let ranges = vec![
        (ShardId(2), EntityIdRange { start: 0, end: 100 }),
        (
            ShardId(0),
            EntityIdRange {
                start: 100,
                end: u64::MAX,
            },
        ),
    ];
    let router = ShardRouter::range(ranges).unwrap();
    let mut shards = router.all_shards();
    shards.sort();
    assert_eq!(shards, vec![ShardId(0), ShardId(2)]);
}

// ── Range validation errors ────────────────────────────────────────────

#[test]
fn range_router_validates_empty_ranges() {
    let err = ShardRouter::range(vec![]).unwrap_err();
    assert!(err.to_string().contains("empty"));
}

#[test]
fn range_router_validates_gap() {
    let ranges = vec![
        (ShardId(0), EntityIdRange { start: 0, end: 100 }),
        // gap: 100..200 is missing
        (
            ShardId(1),
            EntityIdRange {
                start: 200,
                end: u64::MAX,
            },
        ),
    ];
    let err = ShardRouter::range(ranges).unwrap_err();
    match err {
        RouterError::Gap {
            expected: 100,
            found: 200,
        } => {} // correct
        other => panic!("expected Gap, got: {other}"),
    }
}

#[test]
fn range_router_validates_empty_range() {
    let ranges = vec![
        (ShardId(0), EntityIdRange { start: 0, end: 0 }), // empty: start == end
        (
            ShardId(1),
            EntityIdRange {
                start: 0,
                end: u64::MAX,
            },
        ),
    ];
    let err = ShardRouter::range(ranges).unwrap_err();
    match err {
        RouterError::EmptyRange { start: 0 } => {} // correct
        other => panic!("expected EmptyRange, got: {other}"),
    }
}

#[test]
fn range_router_validates_incomplete_coverage() {
    let ranges = vec![
        (ShardId(0), EntityIdRange { start: 0, end: 100 }),
        (
            ShardId(1),
            EntityIdRange {
                start: 100,
                end: 200,
            },
        ),
        // missing: 200..u64::MAX
    ];
    let err = ShardRouter::range(ranges).unwrap_err();
    match err {
        RouterError::IncompleteCoverage { ends_at: 200 } => {} // correct
        other => panic!("expected IncompleteCoverage, got: {other}"),
    }
}

// ── FNV-1a hash ────────────────────────────────────────────────────────

#[test]
fn fnv1a_hash_deterministic() {
    assert_eq!(fnv1a_hash(42), fnv1a_hash(42));
    assert_eq!(fnv1a_hash(0), fnv1a_hash(0));
}

#[test]
fn fnv1a_hash_different_inputs_differ() {
    // Not a guarantee for all inputs, but should hold for small integers.
    assert_ne!(fnv1a_hash(0), fnv1a_hash(1));
    assert_ne!(fnv1a_hash(1), fnv1a_hash(2));
}

// ── Distribution tests ─────────────────────────────────────────────────

#[test]
fn hash_routing_uniform_distribution() {
    // 10K IDs across 5 shards should produce buckets within 15% of expected (2000).
    let router = ShardRouter::hash(5).unwrap();
    let mut counts = [0u64; 5];
    for id in 0u64..10_000 {
        let shard = router.route(EntityId::new(id));
        counts[shard.0 as usize] += 1;
    }
    let expected = 10_000u64 / 5; // 2000
    let max_deviation = expected * 15 / 100; // 15%
    for (i, &count) in counts.iter().enumerate() {
        assert!(
            count >= expected - max_deviation && count <= expected + max_deviation,
            "shard {i} has {count} items, expected {expected} ± {max_deviation}"
        );
    }
}

// ── Property tests ─────────────────────────────────────────────────────

mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn hash_routing_is_deterministic(id in 0u64..u64::MAX) {
            let router = ShardRouter::hash(5).unwrap();
            let entity = EntityId::new(id);
            prop_assert_eq!(router.route(entity), router.route(entity));
        }

        #[test]
        fn hash_routing_stays_in_range(id in 0u64..u64::MAX) {
            let router = ShardRouter::hash(5).unwrap();
            let shard = router.route(EntityId::new(id));
            prop_assert!(shard.0 < 5, "shard {} >= 5", shard);
        }

        #[test]
        fn range_routing_full_range_always_shard_zero(id in 0u64..u64::MAX) {
            let ranges = vec![
                (ShardId(0), EntityIdRange::full()),
            ];
            let router = ShardRouter::range(ranges).unwrap();
            prop_assert_eq!(router.route(EntityId::new(id)), ShardId(0));
        }

        #[test]
        fn entity_id_range_full_contains_everything(id in 0u64..=u64::MAX) {
            let full = EntityIdRange::full();
            prop_assert!(full.contains(id));
        }
    }
}

mod proptest_extended {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        /// For any random u64 ID, routing through a 3-shard range router
        /// always returns the shard whose EntityIdRange contains the ID.
        #[test]
        fn range_routing_routes_to_correct_shard(id in 0u64..u64::MAX) {
            // Split [0, u64::MAX) into 3 shards.
            let boundary1 = u64::MAX / 3;
            let boundary2 = 2 * (u64::MAX / 3);
            let ranges = vec![
                (ShardId(0), EntityIdRange { start: 0, end: boundary1 }),
                (ShardId(1), EntityIdRange { start: boundary1, end: boundary2 }),
                (ShardId(2), EntityIdRange { start: boundary2, end: u64::MAX }),
            ];
            let router = ShardRouter::range(ranges.clone()).unwrap();
            let entity = EntityId::new(id);
            let shard = router.route(entity);
            // Verify the routed shard's range actually contains `id`.
            let (_, range) = ranges.iter().find(|(s, _)| *s == shard).expect("shard in ranges");
            prop_assert!(
                range.contains(id),
                "id={} routed to shard {:?} but range {:?} does not contain it",
                id, shard, range
            );
        }
    }
}
