minerva 0.2.0

Causal ordering for distributed systems
//! Shaped canonical-form refusals for the snapshot frame (the R-9 shaped
//! discipline: start from a valid frame and break one aspect at a time, so
//! every deep branch is certain per case rather than astronomically rare).

extern crate alloc;

use alloc::vec::Vec;

use crate::kairos::Kairos;
use crate::metis::{DotSet, Rhapsody, SnapshotDecodeError};

use super::super::d;

/// A rank whose fields are easy to read in a hex dump.
fn rank(physical: u64, logical: u16) -> Kairos {
    Kairos::new(physical, logical, 1, 0u16)
}

/// The empty visible suffix (an empty have-set frame): version byte plus a
/// zero station count.
fn empty_suffix(bytes: &mut Vec<u8>) {
    bytes.push(0x01u8);
    bytes.extend_from_slice(&0u32.to_be_bytes());
}

/// One valid two-element chain-run frame: station 1, dots 4 and 5, the head
/// at the origin with rank `(100, 0)`, the interior a 50-tick physical
/// advance; nothing visible. The refusal tests mutate exactly one aspect.
fn valid_run_frame() -> Vec<u8> {
    let mut bytes = Vec::new();
    bytes.push(0x01u8);
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&0u32.to_be_bytes());
    // The run row: station 1, base 4, len 2, origin anchor (zero padded),
    // head rank (100, 0).
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&4u64.to_be_bytes());
    bytes.extend_from_slice(&2u32.to_be_bytes());
    bytes.push(0x00);
    bytes.extend_from_slice(&0u32.to_be_bytes());
    bytes.extend_from_slice(&0u64.to_be_bytes());
    bytes.extend_from_slice(&rank(100, 0).to_bytes());
    // One rank step: a 50-tick physical advance.
    bytes.extend_from_slice(&[0, 0, 0, 0, 0, 50]);
    empty_suffix(&mut bytes);
    bytes
}

#[test]
fn test_the_valid_frame_decodes_and_reencodes() {
    let bytes = valid_run_frame();
    let decoded = Rhapsody::from_snapshot_bytes(&bytes).expect("the base frame is valid");
    assert_eq!(decoded.skeleton_len(), 2);
    assert_eq!(decoded.to_snapshot_bytes(), bytes);
}

#[test]
fn test_unknown_version_is_refused() {
    let mut bytes = valid_run_frame();
    bytes[0] = 0x7F;
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::UnknownVersion(0x7F))
    );
}

#[test]
fn test_truncations_are_refused_at_every_boundary() {
    let bytes = valid_run_frame();
    // Any strict prefix must refuse with a length error (never panic); the
    // suffix boundary refuses through the have-set codec's own error.
    for cut in 0..bytes.len() {
        match Rhapsody::from_snapshot_bytes(&bytes[..cut]) {
            Err(SnapshotDecodeError::UnexpectedLength { .. } | SnapshotDecodeError::Visible(_)) => {
            }
            other => panic!("cut {cut}: expected a length refusal, got {other:?}"),
        }
    }
}

#[test]
fn test_trailing_bytes_are_refused_by_from_bytes() {
    let mut bytes = valid_run_frame();
    bytes.push(0xAA);
    assert!(matches!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::UnexpectedLength { .. })
    ));
    // The prefix decode hands the tail back instead.
    let (_, tail) = Rhapsody::from_snapshot_prefix(&bytes).expect("prefix still decodes");
    assert_eq!(tail, &[0xAA]);
}

#[test]
fn test_zero_base_is_refused() {
    let mut bytes = valid_run_frame();
    // The base `u64` sits after the version, counts, and station.
    bytes[13..21].copy_from_slice(&0u64.to_be_bytes());
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::ZeroDot { station: 1 })
    );
}

#[test]
fn test_a_one_element_run_is_refused() {
    let mut bytes = valid_run_frame();
    bytes[21..25].copy_from_slice(&1u32.to_be_bytes());
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::RunTooShort {
            station: 1,
            base: 4
        })
    );
}

#[test]
fn test_a_run_past_the_dot_ceiling_is_refused() {
    let mut bytes = valid_run_frame();
    bytes[13..21].copy_from_slice(&u64::MAX.to_be_bytes());
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::RunOverflow {
            station: 1,
            base: u64::MAX,
            len: 2
        })
    );
}

#[test]
fn test_an_unknown_anchor_tag_is_refused() {
    let mut bytes = valid_run_frame();
    bytes[25] = 0x03;
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::BadAnchorTag { tag: 0x03 })
    );
}

#[test]
fn test_a_padded_origin_anchor_is_refused() {
    let mut bytes = valid_run_frame();
    // The origin head must zero its fixed-width anchor bytes; set one.
    bytes[29] = 0x01;
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::NonCanonicalOriginAnchor {
            station: 1,
            index: 4
        })
    );
}

#[test]
fn test_a_bad_rank_version_is_refused() {
    let mut bytes = valid_run_frame();
    // The head rank frame's version byte.
    bytes[38] = 0x02;
    assert!(matches!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::Rank(_))
    ));
}

#[test]
fn test_a_rank_step_past_the_physical_ceiling_is_refused() {
    let mut bytes = valid_run_frame();
    // Head rank physical to the ceiling, so any positive step overflows.
    bytes[39..47].copy_from_slice(&u64::MAX.to_be_bytes());
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::RankOverflow {
            station: 1,
            index: 5
        })
    );
}

#[test]
fn test_the_rollover_overlap_is_refused_as_non_canonical() {
    let mut bytes = valid_run_frame();
    // Head rank logical at the sentinel's edge: the successor is
    // `(101, 0)`, which a step of one would spell a second way.
    bytes[39..47].copy_from_slice(&100u64.to_be_bytes());
    bytes[47..49].copy_from_slice(&(u16::MAX - 1).to_be_bytes());
    bytes[55..61].copy_from_slice(&[0, 0, 0, 0, 0, 1]);
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::NonCanonicalRankStep {
            station: 1,
            index: 5
        })
    );
    // The canonical spelling of the same value (step zero) is accepted.
    bytes[55..61].copy_from_slice(&[0, 0, 0, 0, 0, 0]);
    let decoded = Rhapsody::from_snapshot_bytes(&bytes).expect("the successor spelling decodes");
    let interior = decoded.locus(d(1, 5)).expect("interior locus");
    assert_eq!(
        (interior.rank.physical(), interior.rank.logical()),
        (101, 0)
    );
}

#[test]
fn test_a_split_chain_is_refused() {
    // Encode a genuinely chained pair as two free loci: the value is
    // legal, this spelling is not (the encoder factors maximal chains).
    let mut bytes = Vec::new();
    bytes.push(0x01u8);
    bytes.extend_from_slice(&0u32.to_be_bytes());
    bytes.extend_from_slice(&2u32.to_be_bytes());
    // Free locus (1, 4): origin anchor, rank (100, 0).
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&4u64.to_be_bytes());
    bytes.push(0x00);
    bytes.extend_from_slice(&rank(100, 0).to_bytes());
    // Free locus (1, 5): anchored AFTER (1, 4), rank a 50-tick advance,
    // exactly the chain law's shape.
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&5u64.to_be_bytes());
    bytes.push(0x01);
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&4u64.to_be_bytes());
    bytes.extend_from_slice(&rank(150, 0).to_bytes());
    empty_suffix(&mut bytes);
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::SplitChain {
            station: 1,
            index: 5
        })
    );
}

#[test]
fn test_an_unchainable_neighbor_pair_rides_free_and_decodes() {
    // The same two free loci, but the second hangs BEFORE its neighbor:
    // dot-consecutive yet not chainable, so the free spelling is the
    // canonical one and the frame decodes.
    let mut bytes = Vec::new();
    bytes.push(0x01u8);
    bytes.extend_from_slice(&0u32.to_be_bytes());
    bytes.extend_from_slice(&2u32.to_be_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&4u64.to_be_bytes());
    bytes.push(0x00);
    bytes.extend_from_slice(&rank(100, 0).to_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&5u64.to_be_bytes());
    bytes.push(0x02);
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&4u64.to_be_bytes());
    bytes.extend_from_slice(&rank(150, 0).to_bytes());
    empty_suffix(&mut bytes);
    let decoded = Rhapsody::from_snapshot_bytes(&bytes).expect("unchainable pair decodes free");
    assert_eq!(decoded.skeleton_len(), 2);
    assert_eq!(decoded.to_snapshot_bytes(), bytes);
}

#[test]
fn test_non_ascending_loci_are_refused() {
    // Two free loci out of order.
    let mut bytes = Vec::new();
    bytes.push(0x01u8);
    bytes.extend_from_slice(&0u32.to_be_bytes());
    bytes.extend_from_slice(&2u32.to_be_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&9u64.to_be_bytes());
    bytes.push(0x00);
    bytes.extend_from_slice(&rank(100, 0).to_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&2u64.to_be_bytes());
    bytes.push(0x00);
    bytes.extend_from_slice(&rank(200, 0).to_bytes());
    empty_suffix(&mut bytes);
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::NonAscendingLoci {
            previous_station: 1,
            previous_index: 9,
            found_station: 1,
            found_index: 2
        })
    );
}

#[test]
fn test_a_run_overlapping_a_free_locus_is_refused() {
    // The valid run covers dots 4 and 5; add a free locus at (1, 5).
    let mut bytes = valid_run_frame();
    let suffix_at = bytes.len() - 5;
    bytes.truncate(suffix_at);
    bytes[5..9].copy_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&5u64.to_be_bytes());
    bytes.push(0x00);
    bytes.extend_from_slice(&rank(999, 0).to_bytes());
    empty_suffix(&mut bytes);
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::NonAscendingLoci {
            previous_station: 1,
            previous_index: 5,
            found_station: 1,
            found_index: 5
        })
    );
}

#[test]
fn test_a_visible_dot_without_a_locus_is_refused() {
    let mut bytes = valid_run_frame();
    // Replace the empty suffix with one naming a dot the sections lack.
    bytes.truncate(bytes.len() - 5);
    let mut visible = DotSet::new();
    assert!(visible.insert(d(9, 1)));
    bytes.extend_from_slice(&visible.to_bytes());
    assert_eq!(
        Rhapsody::from_snapshot_bytes(&bytes),
        Err(SnapshotDecodeError::VisibleWithoutLocus {
            station: 9,
            index: 1
        })
    );
}

#[test]
fn test_a_dangling_anchor_stays_accepted() {
    // The S117 law carries over: a free locus anchored to a dot the frame
    // does not carry is a legal, reachable value, so it must round-trip.
    let mut bytes = Vec::new();
    bytes.push(0x01u8);
    bytes.extend_from_slice(&0u32.to_be_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&1u32.to_be_bytes());
    bytes.extend_from_slice(&7u64.to_be_bytes());
    bytes.push(0x01);
    bytes.extend_from_slice(&6u32.to_be_bytes());
    bytes.extend_from_slice(&123u64.to_be_bytes());
    bytes.extend_from_slice(&rank(100, 0).to_bytes());
    empty_suffix(&mut bytes);
    let decoded = Rhapsody::from_snapshot_bytes(&bytes).expect("a dangling anchor is legal");
    assert_eq!(decoded.to_snapshot_bytes(), bytes);
}