using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Server.Redundancy;

namespace ZB.MOM.WW.OtOpcUa.Server.Tests;

[Trait("Category", "Unit")]
public sealed class ApplyLeaseRegistryTests
{
    private static readonly DateTime T0 = new(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc);

    private sealed class FakeTimeProvider : TimeProvider
    {
        public DateTime Utc { get; set; } = T0;
        public override DateTimeOffset GetUtcNow() => new(Utc, TimeSpan.Zero);
    }

    [Fact]
    public async Task EmptyRegistry_NotInProgress()
    {
        var reg = new ApplyLeaseRegistry();
        reg.IsApplyInProgress.ShouldBeFalse();
        await Task.Yield();
    }

    [Fact]
    public async Task BeginAndDispose_ClosesLease()
    {
        var reg = new ApplyLeaseRegistry();

        await using (reg.BeginApplyLease(1, Guid.NewGuid()))
        {
            reg.IsApplyInProgress.ShouldBeTrue();
            reg.OpenLeaseCount.ShouldBe(1);
        }

        reg.IsApplyInProgress.ShouldBeFalse();
    }

    [Fact]
    public async Task Dispose_OnException_StillCloses()
    {
        var reg = new ApplyLeaseRegistry();
        var publishId = Guid.NewGuid();

        await Should.ThrowAsync<InvalidOperationException>(async () =>
        {
            await using var lease = reg.BeginApplyLease(1, publishId);
            throw new InvalidOperationException("publish failed");
        });

        reg.IsApplyInProgress.ShouldBeFalse("await-using semantics must close the lease on exception");
    }

    [Fact]
    public async Task Dispose_TwiceIsSafe()
    {
        var reg = new ApplyLeaseRegistry();
        var lease = reg.BeginApplyLease(1, Guid.NewGuid());

        await lease.DisposeAsync();
        await lease.DisposeAsync();

        reg.IsApplyInProgress.ShouldBeFalse();
    }

    [Fact]
    public async Task MultipleLeases_Concurrent_StayIsolated()
    {
        var reg = new ApplyLeaseRegistry();
        var id1 = Guid.NewGuid();
        var id2 = Guid.NewGuid();

        await using var lease1 = reg.BeginApplyLease(1, id1);
        await using var lease2 = reg.BeginApplyLease(2, id2);

        reg.OpenLeaseCount.ShouldBe(2);
        await lease1.DisposeAsync();
        reg.IsApplyInProgress.ShouldBeTrue("lease2 still open");
        await lease2.DisposeAsync();
        reg.IsApplyInProgress.ShouldBeFalse();
    }

    [Fact]
    public async Task Watchdog_ClosesStaleLeases()
    {
        var clock = new FakeTimeProvider();
        var reg = new ApplyLeaseRegistry(applyMaxDuration: TimeSpan.FromMinutes(10), timeProvider: clock);

        _ = reg.BeginApplyLease(1, Guid.NewGuid()); // intentional leak; not awaited / disposed

        // Lease still young → no-op.
        clock.Utc = T0.AddMinutes(5);
        reg.PruneStale().ShouldBe(0);
        reg.IsApplyInProgress.ShouldBeTrue();

        // Past the watchdog horizon → force-close.
        clock.Utc = T0.AddMinutes(11);
        var closed = reg.PruneStale();

        closed.ShouldBe(1);
        reg.IsApplyInProgress.ShouldBeFalse("ServiceLevel can't stick at mid-apply after a crashed publisher");
        await Task.Yield();
    }

    [Fact]
    public async Task Watchdog_LeavesRecentLeaseAlone()
    {
        var clock = new FakeTimeProvider();
        var reg = new ApplyLeaseRegistry(applyMaxDuration: TimeSpan.FromMinutes(10), timeProvider: clock);

        await using var lease = reg.BeginApplyLease(1, Guid.NewGuid());
        clock.Utc = T0.AddMinutes(3);

        reg.PruneStale().ShouldBe(0);
        reg.IsApplyInProgress.ShouldBeTrue();
    }
}
