o192 0.2.2

ORION-192: ordered, resilient, independent, URL-safe 192-bit IDs for distributed systems.
Documentation
//! Sortability and remaining edge-case tests.
//!
//! These tests focus on the *sortable* promise of ORION-192:
//!
//!   1. Bytewise lex order of the canonical strings matches the
//!      numeric order of the underlying 192-bit value.
//!   2. Lex order of strings matches chronological order across
//!      `(relative_ms, fraction4096, counter)`.
//!
//! They complement `tests/generator.rs` (which only checks
//! strict-monotonic `prev < next` within one generator) by exercising
//! shuffle/sort invariants, frozen-clock counter spacing, cross-generator
//! ordering, parse round-trips, and the 48-bit timestamp boundary.

mod common;

use std::cmp::Ordering;
use std::collections::HashSet;

use o192::{
    decode_sortable64, encode_sortable64, is_valid, parse, OrionIdError, OrionIdGenerator,
    ID_SIZE_BYTES, ID_SIZE_CHARS, MAX_COUNTER, MAX_RELATIVE_MS,
};

// ───────────────────────────── Helpers ─────────────────────────────

/// Deterministic Mulberry32 PRNG so failures reproduce identically.
struct Mulberry32(u32);

impl Mulberry32 {
    const fn new(seed: u32) -> Self {
        Self(seed)
    }

    fn next_u32(&mut self) -> u32 {
        self.0 = self.0.wrapping_add(0x6d2b_79f5);
        let mut t = self.0;
        t = t.wrapping_mul(t | 1);
        t ^= t.wrapping_add(t.wrapping_mul(t | 0x3d) ^ t);
        t ^ (t >> 14)
    }

    fn next_byte(&mut self) -> u8 {
        (self.next_u32() & 0xff) as u8
    }
}

fn shuffle_in_place<T>(items: &mut [T], rng: &mut Mulberry32) {
    let n = items.len();
    for i in (1..n).rev() {
        let j = (rng.next_u32() as usize) % (i + 1);
        items.swap(i, j);
    }
}

/// A view into a generator's private physical-time hook for deterministic tests.
fn driven_generator() -> OrionIdGenerator {
    OrionIdGenerator::new().with_random_fill(|out| {
        out.fill(0);
        Ok(())
    })
}

// ───────────────────────── 1. Codec lex order = byte order ─────────────────────────

#[test]
fn lex_order_of_encoded_strings_matches_lex_order_of_bytes() {
    let n: usize = 5_000;
    let mut rng = Mulberry32::new(0xC0FF_EE42);

    let mut buffers: Vec<[u8; ID_SIZE_BYTES]> = Vec::with_capacity(n);
    for _ in 0..n {
        let mut buf = [0u8; ID_SIZE_BYTES];
        for byte in &mut buf {
            *byte = rng.next_byte();
        }
        buffers.push(buf);
    }

    let mut by_bytes = buffers.clone();
    by_bytes.sort_unstable();

    let mut paired: Vec<(String, [u8; ID_SIZE_BYTES])> = buffers
        .iter()
        .map(|b| (encode_sortable64(b).unwrap(), *b))
        .collect();
    paired.sort_by(|a, b| a.0.cmp(&b.0));

    for (i, (expected, (_, actual))) in by_bytes.iter().zip(paired.iter()).enumerate() {
        assert_eq!(expected, actual, "divergence at rank {i}");
    }
}

// ───────────────────────── 2. Lex order = (ms, frac, counter) tuple ─────────────────────────

#[test]
fn lex_order_matches_relative_ms_fraction_counter_tuple() {
    let mut gen = driven_generator();
    let n = 2_000u128;
    let mut ids: Vec<String> = Vec::with_capacity(n as usize);

    for i in 0..n {
        let ms: u128 = 1_000 + i;
        let frac: u128 = (i * 137) & 0xfff;
        gen.set_physical_time_key_for_test((ms << 12) | frac);
        ids.push(gen.next().unwrap());
    }

    for i in 1..ids.len() {
        assert!(ids[i - 1] < ids[i], "not strictly monotonic at {i}");
    }

    let mut by_tuple: Vec<(u128, u16, u32, String)> = ids
        .iter()
        .map(|id| {
            let view = parse(id, 0).unwrap();
            (
                view.relative_ms,
                view.fraction4096,
                view.counter,
                id.clone(),
            )
        })
        .collect();
    let mut by_string: Vec<String> = ids.clone();
    by_tuple.sort_by_key(|entry| (entry.0, entry.1, entry.2));
    by_string.sort();

    for (i, (tuple_entry, string_entry)) in by_tuple.iter().zip(by_string.iter()).enumerate() {
        assert_eq!(&tuple_entry.3, string_entry, "divergence at {i}");
    }
}

// ───────────────────────── 3. Bulk-sort round-trip ─────────────────────────

#[test]
fn shuffled_ids_sort_back_to_generation_order() {
    let mut gen = OrionIdGenerator::new();
    let n = 10_000usize;
    let original: Vec<String> = (0..n).map(|_| gen.next().unwrap()).collect();

    let mut shuffled = original.clone();
    let mut rng = Mulberry32::new(0x9E37_79B9);
    shuffle_in_place(&mut shuffled, &mut rng);

    shuffled.sort();
    for (i, (expected, actual)) in original.iter().zip(shuffled.iter()).enumerate() {
        assert_eq!(expected, actual, "sort mismatch at rank {i}");
    }
}

// ───────────────────────── 4. Frozen clock → counter +1 ─────────────────────────

#[test]
fn frozen_clock_makes_counter_increment_by_exactly_one() {
    let mut gen = driven_generator();
    gen.set_physical_time_key_for_test(42u128 << 12);

    let first = parse(&gen.next().unwrap(), 0).unwrap();
    let initial = first.counter;

    for i in 1..1_000 {
        let view = parse(&gen.next().unwrap(), 0).unwrap();
        assert_eq!(
            view.counter,
            initial + i as u32,
            "counter not +1 at step {i}"
        );
        assert_eq!(view.relative_ms, 42, "relative_ms must stay frozen");
        assert_eq!(view.fraction4096, 0, "fraction must stay frozen");
    }
}

// ───────────────────────── 5. Cross-generator ordering ─────────────────────────

#[test]
fn two_generators_interleave_correctly_under_external_clock() {
    let mut gen_a = driven_generator();
    let mut gen_b = driven_generator();

    let mut entries: Vec<(u32, String)> = Vec::new();
    for tick in 0u32..500 {
        let key: u128 = u128::from(tick + 1) << 12;
        gen_a.set_physical_time_key_for_test(key);
        gen_b.set_physical_time_key_for_test(key);
        entries.push((tick, gen_a.next().unwrap()));
        entries.push((tick, gen_b.next().unwrap()));
    }

    entries.sort_by(|a, b| a.1.cmp(&b.1));
    for i in 1..entries.len() {
        assert!(
            entries[i - 1].0 <= entries[i].0,
            "sorted ordering inverted ticks at {i}",
        );
    }
}

// ───────────────────────── 6. Parse round-trip ─────────────────────────

#[test]
fn parse_fields_reencode_back_to_original_bytes() {
    let mut gen = OrionIdGenerator::new();
    for i in 0..200 {
        let id = gen.next().unwrap();
        let view = parse(&id, 0).unwrap();

        let mut out = [0u8; ID_SIZE_BYTES];
        let mut ms = view.relative_ms;
        for offset in (0..6).rev() {
            out[offset] = (ms & 0xff) as u8;
            ms >>= 8;
        }
        out[6] = ((view.fraction4096 >> 4) & 0xff) as u8;
        out[7] = (((view.fraction4096 & 0x0f) as u8) << 4) | (((view.counter >> 16) & 0x0f) as u8);
        out[8] = ((view.counter >> 8) & 0xff) as u8;
        out[9] = (view.counter & 0xff) as u8;
        let random_bytes = common::hex_to_bytes(&view.random_hex);
        out[10..24].copy_from_slice(&random_bytes);

        assert_eq!(out, view.bytes, "byte mismatch at iter {i}");
        assert_eq!(
            encode_sortable64(&out).unwrap(),
            id,
            "encode mismatch at iter {i}"
        );
    }
}

// ───────────────────────── 7. 48-bit relative_ms boundary ─────────────────────────

#[test]
fn relative_ms_close_to_48bit_max_still_produces_valid_id() {
    let mut gen = driven_generator();
    gen.set_physical_time_key_for_test((MAX_RELATIVE_MS - 1) << 12);

    let id = gen.next().unwrap();
    assert_eq!(id.len(), ID_SIZE_CHARS);
    assert!(is_valid(&id));

    let view = parse(&id, 0).unwrap();
    assert_eq!(view.relative_ms, MAX_RELATIVE_MS - 1);
    assert_eq!(view.fraction4096, 0);
}

// ───────────────────────── 8. Whitespace inputs rejected ─────────────────────────

#[test]
fn whitespace_framing_inputs_are_rejected() {
    let zeros = "0".repeat(ID_SIZE_CHARS);

    let cases = [
        format!(" {zeros}"),
        format!("{zeros} "),
        format!("{zeros}\n"),
        format!("\n{zeros}"),
    ];
    for id in &cases {
        assert!(decode_sortable64(id).is_err(), "accepted: {id:?}");
        assert!(!is_valid(id));
    }

    let all_spaces = " ".repeat(ID_SIZE_CHARS);
    assert_eq!(
        decode_sortable64(&all_spaces),
        Err(OrionIdError::InvalidCharacter),
    );
}

// ───────────────────────── 9. Random tail entropy smoke ─────────────────────────

#[test]
fn random_tail_differs_across_ids_from_one_generator() {
    let mut gen = OrionIdGenerator::new();
    let mut tails: HashSet<String> = HashSet::with_capacity(1_000);
    for _ in 0..1_000 {
        tails.insert(parse(&gen.next().unwrap(), 0).unwrap().random_hex);
    }
    assert!(
        tails.len() >= 999,
        "random tail entropy too low: only {}/1000 unique",
        tails.len(),
    );
}

// ───────────────────────── 10. Counter-overflow monotonicity ─────────────────────────

#[test]
fn counter_overflow_under_frozen_clock_stays_strictly_monotonic() {
    let mut gen = driven_generator();
    gen.set_physical_time_key_for_test(7u128 << 12);

    let mut prev = gen.next().unwrap();
    let iterations = MAX_COUNTER + 100;
    for i in 0..iterations {
        let cur = gen.next().unwrap();
        assert_eq!(
            prev.as_str().cmp(cur.as_str()),
            Ordering::Less,
            "counter overflow broke lex order at iteration {i}",
        );
        prev = cur;
    }

    assert!(
        gen.synthetic_ticks() >= 1,
        "expected at least one synthetic tick"
    );
}

// ───────────────────────── Bonus: boundary-counter vectors ─────────────────────────

#[test]
fn boundary_counter_vectors_sort_by_counter_regardless_of_random_tail() {
    // counter=100, random=0xff*14
    let a_id = "000000FdW000PF~~~~~~~~~~~~~~~~~~";
    // counter=101, random=0x00*14
    let b_id = "000000FdW000PG000000000000000000";
    assert!(
        a_id < b_id,
        "counter must dominate the random tail in lex order"
    );

    let view_a = parse(a_id, 0).unwrap();
    let view_b = parse(b_id, 0).unwrap();
    assert_eq!(view_a.relative_ms, view_b.relative_ms);
    assert_eq!(view_a.fraction4096, view_b.fraction4096);
    assert_eq!(view_a.counter + 1, view_b.counter);
}