using ZB.MOM.WW.Overview.Polling;

namespace ZB.MOM.WW.Overview.Tests;

/// <summary>
/// The store is the cache-first seam: a page reads it and never probes. These tests pin that it
/// always has something to serve and that subscribers hear about replacements.
/// </summary>
public class OverviewSnapshotStoreTests
{
    private static OverviewSnapshot SnapshotAt(int second) =>
        new(new DateTimeOffset(2026, 7, 24, 12, 0, second, TimeSpan.Zero), []);

    [Fact]
    public void Current_BeforeAnyPublish_IsEmptyNotNull()
    {
        // A page can render before the poller's first sweep finishes; a null here would be a
        // NullReferenceException on the landing page of a monitoring dashboard.
        var store = new OverviewSnapshotStore();

        Assert.NotNull(store.Current);
        Assert.Empty(store.Current.Applications);
    }

    [Fact]
    public void Publish_ReplacesCurrent()
    {
        var store = new OverviewSnapshotStore();
        var snapshot = SnapshotAt(1);

        store.Publish(snapshot);

        Assert.Same(snapshot, store.Current);
    }

    [Fact]
    public void Publish_NotifiesSubscribersWithTheNewSnapshot()
    {
        var store = new OverviewSnapshotStore();
        OverviewSnapshot? received = null;
        store.Changed += s => received = s;

        var snapshot = SnapshotAt(2);
        store.Publish(snapshot);

        Assert.Same(snapshot, received);
    }

    [Fact]
    public void Publish_AfterUnsubscribe_DoesNotNotify()
    {
        // Blazor circuits come and go; a handler that outlives its component would call
        // StateHasChanged on a disposed renderer on every sweep.
        var store = new OverviewSnapshotStore();
        var calls = 0;
        void Handler(OverviewSnapshot _) => calls++;

        store.Changed += Handler;
        store.Publish(SnapshotAt(1));
        store.Changed -= Handler;
        store.Publish(SnapshotAt(2));

        Assert.Equal(1, calls);
    }

    [Fact]
    public void Publish_Null_Throws()
    {
        Assert.Throws<ArgumentNullException>(() => new OverviewSnapshotStore().Publish(null!));
    }

    [Fact]
    public async Task Current_ReadDuringPublish_ObservesOneWholeSnapshot()
    {
        // The tearing guarantee: a reader always sees a complete graph, never a half-swapped one.
        var store = new OverviewSnapshotStore();
        var a = SnapshotAt(1);
        var b = SnapshotAt(2);
        store.Publish(a);

        using var stop = new CancellationTokenSource();
        var reader = Task.Run(() =>
        {
            while (!stop.IsCancellationRequested)
            {
                var seen = store.Current;
                Assert.True(ReferenceEquals(seen, a) || ReferenceEquals(seen, b));
            }
        });

        for (var i = 0; i < 1_000; i++)
            store.Publish(i % 2 == 0 ? b : a);

        await stop.CancelAsync();
        await reader;
    }
}
