using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Stability;

namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Stability;

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

    /// <summary>Verifies constructor throws for Tier A or B.</summary>
    /// <param name="tier">The driver tier to test.</param>
    [Theory]
    [InlineData(DriverTier.A)]
    [InlineData(DriverTier.B)]
    public void TierAOrB_Ctor_Throws(DriverTier tier)
    {
        var supervisor = new FakeSupervisor();
        Should.Throw<ArgumentException>(() => new ScheduledRecycleScheduler(
            tier, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance));
    }

    /// <summary>Verifies constructor throws for zero or negative intervals.</summary>
    [Fact]
    public void ZeroOrNegativeInterval_Throws()
    {
        var supervisor = new FakeSupervisor();
        Should.Throw<ArgumentException>(() => new ScheduledRecycleScheduler(
            DriverTier.C, TimeSpan.Zero, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance));
        Should.Throw<ArgumentException>(() => new ScheduledRecycleScheduler(
            DriverTier.C, TimeSpan.FromSeconds(-1), T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance));
    }

    /// <summary>Verifies Tick before the next recycle time is a no-op.</summary>
    [Fact]
    public async Task Tick_BeforeNextRecycle_NoOp()
    {
        var supervisor = new FakeSupervisor();
        var sch = new ScheduledRecycleScheduler(DriverTier.C, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);

        var fired = await sch.TickAsync(T0 + TimeSpan.FromDays(6), CancellationToken.None);

        fired.ShouldBeFalse();
        supervisor.RecycleCount.ShouldBe(0);
    }

    /// <summary>Verifies Tick at or after the next recycle time fires once and advances.</summary>
    [Fact]
    public async Task Tick_AtOrAfterNextRecycle_FiresOnce_AndAdvances()
    {
        var supervisor = new FakeSupervisor();
        var sch = new ScheduledRecycleScheduler(DriverTier.C, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);

        var fired = await sch.TickAsync(T0 + Weekly + TimeSpan.FromMinutes(1), CancellationToken.None);

        fired.ShouldBeTrue();
        supervisor.RecycleCount.ShouldBe(1);
        sch.NextRecycleUtc.ShouldBe(T0 + Weekly + Weekly);
    }

    /// <summary>Verifies RequestRecycleNow fires immediately without advancing the schedule.</summary>
    [Fact]
    public async Task RequestRecycleNow_Fires_Immediately_WithoutAdvancingSchedule()
    {
        var supervisor = new FakeSupervisor();
        var sch = new ScheduledRecycleScheduler(DriverTier.C, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);
        var nextBefore = sch.NextRecycleUtc;

        await sch.RequestRecycleNowAsync("memory hard-breach", CancellationToken.None);

        supervisor.RecycleCount.ShouldBe(1);
        supervisor.LastReason.ShouldBe("memory hard-breach");
        sch.NextRecycleUtc.ShouldBe(nextBefore, "ad-hoc recycle doesn't shift the cron schedule");
    }

    /// <summary>Verifies multiple ticks across the recycle interval each advance by one interval.</summary>
    [Fact]
    public async Task MultipleFires_AcrossTicks_AdvanceOneIntervalEach()
    {
        var supervisor = new FakeSupervisor();
        var sch = new ScheduledRecycleScheduler(DriverTier.C, TimeSpan.FromDays(1), T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);

        await sch.TickAsync(T0 + TimeSpan.FromDays(1) + TimeSpan.FromHours(1), CancellationToken.None);
        await sch.TickAsync(T0 + TimeSpan.FromDays(2) + TimeSpan.FromHours(1), CancellationToken.None);
        await sch.TickAsync(T0 + TimeSpan.FromDays(3) + TimeSpan.FromHours(1), CancellationToken.None);

        supervisor.RecycleCount.ShouldBe(3);
        sch.NextRecycleUtc.ShouldBe(T0 + TimeSpan.FromDays(4));
    }

    /// <summary>Fake driver supervisor for testing.</summary>
    private sealed class FakeSupervisor : IDriverSupervisor
    {
        /// <summary>Gets the driver instance ID.</summary>
        public string DriverInstanceId => "tier-c-fake";

        /// <summary>Gets the number of times RecycleAsync was called.</summary>
        public int RecycleCount { get; private set; }

        /// <summary>Gets the reason from the most recent recycle call.</summary>
        public string? LastReason { get; private set; }

        /// <summary>Simulates a driver recycle operation.</summary>
        /// <param name="reason">The reason for the recycle.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A completed task.</returns>
        public Task RecycleAsync(string reason, CancellationToken cancellationToken)
        {
            RecycleCount++;
            LastReason = reason;
            return Task.CompletedTask;
        }
    }
}
