using System.Collections.Concurrent;
using ZB.MOM.WW.LocalDb.Hlc;

namespace ZB.MOM.WW.LocalDb.Tests;

public sealed class HybridLogicalClockTests
{
    [Fact]
    public void Next_IsStrictlyMonotonic()
    {
        var clock = new HybridLogicalClock();
        var prev = clock.Next();
        for (var i = 0; i < 1000; i++)
        {
            var next = clock.Next();
            Assert.True(next > prev, $"call {i}: {next} !> {prev}");
            prev = next;
        }
    }

    [Fact]
    public void Next_UsesUtcPhysicalMs()
    {
        const long fixedMs = 1_700_000_000_123;
        var clock = new HybridLogicalClock(utcNowMs: () => fixedMs);

        Assert.Equal(fixedMs, HybridLogicalClock.PhysicalMs(clock.Next()));
    }

    [Fact]
    public void Next_SameMs_IncrementsCounter()
    {
        const long fixedMs = 1_700_000_000_000;
        var clock = new HybridLogicalClock(utcNowMs: () => fixedMs);

        var first = clock.Next();
        var second = clock.Next();
        var third = clock.Next();

        Assert.Equal(0, HybridLogicalClock.Counter(first));
        Assert.Equal(1, HybridLogicalClock.Counter(second));
        Assert.Equal(2, HybridLogicalClock.Counter(third));
    }

    [Fact]
    public void Observe_RemoteAhead_AdvancesPastRemote()
    {
        // Frozen wall clock well behind the remote stamp.
        var clock = new HybridLogicalClock(utcNowMs: () => 1_000);
        var remote = 5_000_000_000L << 16;

        clock.Observe(remote);

        Assert.True(clock.Next() > remote);
    }

    [Fact]
    public void Observe_RemoteBehind_NoRegression()
    {
        var clock = new HybridLogicalClock(utcNowMs: () => 1_700_000_000_000);
        var current = clock.Next();

        clock.Observe(current - 1_000_000); // a stamp from the past

        Assert.Equal(current, clock.Current);
    }

    [Fact]
    public void Resume_FromPersistedValue_NeverRegresses()
    {
        // Persisted value is far ahead of what the (injected) wall clock yields.
        var persisted = (2_000_000_000_000L << 16) | 0x00FF;
        var clock = new HybridLogicalClock(initialValue: persisted, utcNowMs: () => 1_000);

        Assert.True(clock.Next() > persisted);
    }

    [Fact]
    public void CounterOverflow_RollsPhysicalForward()
    {
        const long ms = 1_700_000_000_000;
        // Counter already maxed out under a frozen clock at the same physical ms.
        var maxed = (ms << 16) | 0xFFFF;
        var clock = new HybridLogicalClock(initialValue: maxed, utcNowMs: () => ms);

        var next = clock.Next();

        Assert.Equal(ms + 1, HybridLogicalClock.PhysicalMs(next));
        Assert.Equal(0, HybridLogicalClock.Counter(next));
    }

    [Fact]
    public void Next_ConcurrentCallers_NoDuplicates_StrictlyIncreasing()
    {
        var clock = new HybridLogicalClock();
        var results = new ConcurrentBag<long>();

        Parallel.For(0, 8, _ =>
        {
            for (var i = 0; i < 1000; i++)
                results.Add(clock.Next());
        });

        Assert.Equal(8000, results.Count);
        Assert.Equal(8000, results.Distinct().Count());
    }
}
