namespace Just.Core.Tests.GuidV8Tests; public class ExtractTimestamp { [Fact] public void RoundTrip_ShouldReturnOriginalTimestamp() { var original = new DateTime(2026, 7, 10, 21, 30, 0, DateTimeKind.Utc); var guid = GuidV8.NewGuid(original, RngEntropy.Weak); var extracted = GuidV8.ExtractTimestamp(guid); extracted.ShouldBe(original); extracted.Kind.ShouldBe(DateTimeKind.Utc); } [Fact] public void RoundTrip_ShouldPreserve100MicrosecondPrecision() { // 100-microsecond precision: 1234567 * 100µs = 123456.7ms var original = new DateTime(2024, 3, 15, 8, 45, 30, 123, DateTimeKind.Utc) .AddTicks(4567); // sub-millisecond ticks var guid = GuidV8.NewGuid(original, RngEntropy.Weak); var extracted = GuidV8.ExtractTimestamp(guid); // 123ms + 4567 ticks = 123ms + 456.7µs // timestamp = (ticks since epoch) / 1000 (100µs units) // Ticks: 123ms = 1,230,000 ticks; + 4,567 ticks = 1,234,567 ticks // timestamp = 1,234,567 / 1000 = 1234 (truncates last 3 digits) // recovered = 1234 * 1000 = 1,234,000 ticks = 123.4ms // So the sub-100µs part (67 ticks) is lost extracted.ShouldBe(new DateTime(2024, 3, 15, 8, 45, 30, 123, DateTimeKind.Utc) .AddTicks(4000)); // 123.4ms } [Fact] public void RoundTrip_UnixEpoch_ShouldReturnExactEpoch() { var original = DateTime.UnixEpoch; var guid = GuidV8.NewGuid(original, RngEntropy.Weak); var extracted = GuidV8.ExtractTimestamp(guid); extracted.ShouldBe(original); } [Fact] public void RoundTrip_PreUnixEpoch_ShouldThrowArgumentException() { // Negative timestamps cannot be encoded correctly because the uint casts // in NewGuid corrupt the sign. The encoding should reject them. var preEpoch = new DateTime(1969, 7, 20, 20, 17, 40, DateTimeKind.Utc); Action act = () => GuidV8.NewGuid(preEpoch); act.ShouldThrow(); } [Fact] public void RoundTrip_FarFuture_ShouldPreserveTimestamp() { // Year 2200 — well within 48-bit range var original = new DateTime(2200, 1, 1, 0, 0, 0, DateTimeKind.Utc); var guid = GuidV8.NewGuid(original, RngEntropy.Weak); var extracted = GuidV8.ExtractTimestamp(guid); extracted.ShouldBe(original); } [Fact] public void ExtractTimestamp_WithStrongEntropy_ShouldStillRecoverTimestamp() { var original = new DateTime(2025, 12, 25, 10, 0, 0, DateTimeKind.Utc); var guid = GuidV8.NewGuid(original, RngEntropy.Strong); var extracted = GuidV8.ExtractTimestamp(guid); extracted.ShouldBe(original); } [Fact] public void NonV8Guid_ShouldThrowArgumentException() { var guid = Guid.NewGuid(); // UUID v4 Action act = () => GuidV8.ExtractTimestamp(guid); act.ShouldThrow() .WithMessage("The provided GUID is not a UUID v8 (version=4). (Parameter 'guid')"); } [Fact] public void GuidEmpty_ShouldThrowArgumentException() { Action act = () => GuidV8.ExtractTimestamp(Guid.Empty); act.ShouldThrow() .WithMessage("The provided GUID is not a UUID v8 (version=0). (Parameter 'guid')"); } [Fact] public void MultipleRoundTrips_ShouldAllMatch() { var baseTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); for (int i = 0; i < 100; i++) { var original = baseTime.AddMinutes(i * 37 + 13); // varied intervals var guid = GuidV8.NewGuid(original, RngEntropy.Weak); var extracted = GuidV8.ExtractTimestamp(guid); extracted.ShouldBe(original); } } [Fact] public void ExtractTimestamp_ThenNewGuid_ShouldProduceSortableSequence() { var t1 = new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc); var t2 = t1.AddSeconds(1); var t3 = t2.AddSeconds(1); var g1 = GuidV8.NewGuid(t1, RngEntropy.Weak); var g2 = GuidV8.NewGuid(t2, RngEntropy.Weak); var g3 = GuidV8.NewGuid(t3, RngEntropy.Weak); var extracted1 = GuidV8.ExtractTimestamp(g1); var extracted2 = GuidV8.ExtractTimestamp(g2); var extracted3 = GuidV8.ExtractTimestamp(g3); extracted1.ShouldBe(t1); extracted2.ShouldBe(t2); extracted3.ShouldBe(t3); // Verify they sort in order new[] { g3, g1, g2 }.Order().ShouldBe([g1, g2, g3]); new[] { extracted3, extracted1, extracted2 }.Order().ShouldBe([t1, t2, t3]); } }