namespace Just.Core.Tests.SeqIdTests; public class NextId { private static readonly DateTime TestEpoch = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); private readonly SeqId _seqId = new(TestEpoch); private const int TimestampShift = 22; // 63 - 41 = 22 private const long TimestampMask = 0x1FFFFFFFFFF; // 41 bits mask private const int SeqShift = 14; // TimestampShift - 8 = 14 private const long SeqMask = 0xFF; // 8 bits mask private const long RandMask = 0x3FFF; // 14 bits mask (since 2^14 = 16384) [Fact] public void NextId_ShouldHaveCorrectBitStructure() { // Arrange var time = TestEpoch.AddMilliseconds(500); // Act long id = _seqId.Next(time); // Assert long timestampPart = (id >> TimestampShift) & TimestampMask; long sequencePart = (id >> SeqShift) & SeqMask; long randomPart = id & RandMask; timestampPart.Should().Be(500); sequencePart.Should().Be(0); randomPart.Should().BeInRange(0, RandMask); } [Fact] public void NextId_ShouldIncrementSequenceForSameTimestamp() { // Arrange var time = TestEpoch.AddMilliseconds(100); // Act long id1 = _seqId.Next(time); long id2 = _seqId.Next(time); // Assert long sequence1 = (id1 >> SeqShift) & SeqMask; long sequence2 = (id2 >> SeqShift) & SeqMask; sequence2.Should().Be(sequence1 + 1); } [Fact] public void NextId_ShouldResetSequenceForNewTimestamp() { // Arrange var time1 = TestEpoch.AddMilliseconds(100); var time2 = time1.AddMilliseconds(1); // Act _ = _seqId.Next(time1); // Sequence increments to 0 _ = _seqId.Next(time1); // Sequence increments to 1 long id = _seqId.Next(time2); // Should reset to 0 // Assert long sequence = (id >> SeqShift) & SeqMask; sequence.Should().Be(0); } [Fact] public void NextId_ShouldThrowWhenTimestampDecreases() { // Arrange var time1 = TestEpoch.AddMilliseconds(200); var time2 = TestEpoch.AddMilliseconds(100); // Act & Assert _seqId.Next(time1); // First call sets last timestamp Action act = () => _seqId.Next(time2); act.Should().Throw() .WithMessage("Refused to create new SeqId. Last timestamp is in the future."); } [Fact] public void NextId_ShouldThrowWhenSequenceExhausted() { // Arrange var time = TestEpoch.AddMilliseconds(200); // Act & Assert for (int i = 0; i < 255; i++) { _seqId.Next(time); // Exhauste sequence } Action act = () => _seqId.Next(time); act.Should().Throw() .WithMessage("Refused to create new SeqId. Sequence exhausted."); } [Fact] public void NextId_WithStrongEntropy_ShouldSetLower14Bits() { // Arrange var time = TestEpoch.AddMilliseconds(300); // Act long id = _seqId.Next(time, RngEntropy.Strong); // Assert long randomPart = id & RandMask; randomPart.Should().BeInRange(0, RandMask); } [Fact] public void NextId_WithWeakEntropy_ShouldSetLower14Bits() { // Arrange var time = TestEpoch.AddMilliseconds(400); // Act long id = _seqId.Next(time, RngEntropy.Weak); // Assert long randomPart = id & RandMask; randomPart.Should().BeInRange(0, RandMask); } [Fact] public void DefaultInstance_NextId_ShouldUseDefaultEpoch() { // Arrange var now = DateTime.UtcNow; var defaultEpoch = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); long expectedTimestamp = (long)(now - defaultEpoch).TotalMilliseconds; // Act long id = SeqId.NextId(); // Assert long timestampPart = (id >> TimestampShift) & TimestampMask; timestampPart.Should().BeCloseTo(expectedTimestamp & TimestampMask, 1); // Mask handles overflow } }