using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Browsing;

/// <summary>Unit tests for <see cref="BrowseSessionReaper"/>. We drive the reaper
/// directly through the internal <c>ReapOnceAsync</c> entry point and skew
/// <see cref="FakeBrowseSession.LastUsedUtc"/> to simulate the passage of time —
/// no real wall-clock waits.</summary>
public sealed class BrowseSessionReaperTests
{
    private static BrowseSessionReaper NewReaper(BrowseSessionRegistry registry) =>
        new(registry, NullLogger<BrowseSessionReaper>.Instance);

    [Fact]
    public async Task ReapOnceAsync_evicts_idle_session()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession
        {
            LastUsedUtc = DateTime.UtcNow - TimeSpan.FromMinutes(3),
        };
        registry.Register(session);
        var reaper = NewReaper(registry);

        await reaper.ReapOnceAsync(CancellationToken.None);

        registry.TryGet(session.Token, out _).ShouldBeFalse();
        session.Disposed.ShouldBeTrue();
    }

    [Fact]
    public async Task ReapOnceAsync_preserves_recent_session()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession { LastUsedUtc = DateTime.UtcNow };
        registry.Register(session);
        var reaper = NewReaper(registry);

        await reaper.ReapOnceAsync(CancellationToken.None);

        registry.TryGet(session.Token, out _).ShouldBeTrue();
        session.Disposed.ShouldBeFalse();
    }

    [Fact]
    public async Task ReapOnceAsync_handles_already_removed_session()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession
        {
            LastUsedUtc = DateTime.UtcNow - TimeSpan.FromMinutes(3),
        };
        registry.Register(session);
        // Race the reaper: pull the entry out before the loop calls TryRemove.
        registry.TryRemove(session.Token, out _).ShouldBeTrue();
        var reaper = NewReaper(registry);

        // No exception, no double dispose. Because we removed the session ourselves
        // without disposing it, Disposed should still be false.
        await reaper.ReapOnceAsync(CancellationToken.None);

        session.Disposed.ShouldBeFalse();
    }

    [Fact]
    public async Task ReapOnceAsync_continues_when_one_session_dispose_throws()
    {
        var registry = new BrowseSessionRegistry();
        var staleAt = DateTime.UtcNow - TimeSpan.FromMinutes(3);
        var bad = new FakeBrowseSession { LastUsedUtc = staleAt, ThrowOnDispose = true };
        var good = new FakeBrowseSession { LastUsedUtc = staleAt };
        registry.Register(bad);
        registry.Register(good);
        var reaper = NewReaper(registry);

        // The dispose exception is swallowed; the second session must still be reaped + disposed.
        await Should.NotThrowAsync(() => reaper.ReapOnceAsync(CancellationToken.None));

        registry.TryGet(bad.Token, out _).ShouldBeFalse();
        registry.TryGet(good.Token, out _).ShouldBeFalse();
        good.Disposed.ShouldBeTrue();
    }
}
