using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.OtOpcUa.Server.Hosting;

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

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

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

    private sealed class InMemoryDbContextFactory : IDbContextFactory<OtOpcUaConfigDbContext>
    {
        private readonly DbContextOptions<OtOpcUaConfigDbContext> _options;
        public InMemoryDbContextFactory(string dbName)
        {
            _options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
                .UseInMemoryDatabase(dbName)
                .Options;
        }
        public OtOpcUaConfigDbContext CreateDbContext() => new(_options);
    }

    private readonly string _dbName = $"resilience-pub-{Guid.NewGuid():N}";
    private readonly InMemoryDbContextFactory _factory;
    private readonly OtOpcUaConfigDbContext _readCtx;

    public ResilienceStatusPublisherHostedServiceTests()
    {
        _factory = new InMemoryDbContextFactory(_dbName);
        _readCtx = _factory.CreateDbContext();
    }

    public void Dispose() => _readCtx.Dispose();

    [Fact]
    public async Task EmptyTracker_Tick_NoOp_NoRowsWritten()
    {
        var tracker = new DriverResilienceStatusTracker();
        var host = new ResilienceStatusPublisherHostedService(
            tracker, _factory, NullLogger<ResilienceStatusPublisherHostedService>.Instance);

        await host.PersistOnceAsync(CancellationToken.None);

        host.TickCount.ShouldBe(1);
        (await _readCtx.DriverInstanceResilienceStatuses.CountAsync()).ShouldBe(0);
    }

    [Fact]
    public async Task SingleHost_OnePairWithCounters_UpsertsNewRow()
    {
        var clock = new FakeClock();
        var tracker = new DriverResilienceStatusTracker();
        tracker.RecordFailure("drv-1", "plc-a", T0);
        tracker.RecordFailure("drv-1", "plc-a", T0);
        tracker.RecordBreakerOpen("drv-1", "plc-a", T0.AddSeconds(1));

        var host = new ResilienceStatusPublisherHostedService(
            tracker, _factory, NullLogger<ResilienceStatusPublisherHostedService>.Instance,
            timeProvider: clock);

        clock.Utc = T0.AddSeconds(2);
        await host.PersistOnceAsync(CancellationToken.None);

        var row = await _readCtx.DriverInstanceResilienceStatuses.SingleAsync();
        row.DriverInstanceId.ShouldBe("drv-1");
        row.HostName.ShouldBe("plc-a");
        row.ConsecutiveFailures.ShouldBe(2);
        row.LastCircuitBreakerOpenUtc.ShouldBe(T0.AddSeconds(1));
        row.LastSampledUtc.ShouldBe(T0.AddSeconds(2));
    }

    [Fact]
    public async Task SecondTick_UpdatesExistingRow_InPlace()
    {
        var clock = new FakeClock();
        var tracker = new DriverResilienceStatusTracker();
        tracker.RecordFailure("drv-1", "plc-a", T0);

        var host = new ResilienceStatusPublisherHostedService(
            tracker, _factory, NullLogger<ResilienceStatusPublisherHostedService>.Instance,
            timeProvider: clock);

        clock.Utc = T0.AddSeconds(5);
        await host.PersistOnceAsync(CancellationToken.None);

        // Second tick: success resets the counter.
        tracker.RecordSuccess("drv-1", "plc-a", T0.AddSeconds(6));
        clock.Utc = T0.AddSeconds(10);
        await host.PersistOnceAsync(CancellationToken.None);

        (await _readCtx.DriverInstanceResilienceStatuses.CountAsync()).ShouldBe(1, "one row, updated in place");
        var row = await _readCtx.DriverInstanceResilienceStatuses.SingleAsync();
        row.ConsecutiveFailures.ShouldBe(0);
        row.LastSampledUtc.ShouldBe(T0.AddSeconds(10));
    }

    [Fact]
    public async Task MultipleHosts_BothPersist_Independently()
    {
        var tracker = new DriverResilienceStatusTracker();
        tracker.RecordFailure("drv-1", "plc-a", T0);
        tracker.RecordFailure("drv-1", "plc-a", T0);
        tracker.RecordFailure("drv-1", "plc-b", T0);

        var host = new ResilienceStatusPublisherHostedService(
            tracker, _factory, NullLogger<ResilienceStatusPublisherHostedService>.Instance);

        await host.PersistOnceAsync(CancellationToken.None);

        var rows = await _readCtx.DriverInstanceResilienceStatuses
            .OrderBy(r => r.HostName)
            .ToListAsync();
        rows.Count.ShouldBe(2);
        rows[0].HostName.ShouldBe("plc-a");
        rows[0].ConsecutiveFailures.ShouldBe(2);
        rows[1].HostName.ShouldBe("plc-b");
        rows[1].ConsecutiveFailures.ShouldBe(1);
    }

    [Fact]
    public async Task FootprintCounters_Persist()
    {
        var tracker = new DriverResilienceStatusTracker();
        tracker.RecordFootprint("drv-1", "plc-a",
            baselineBytes: 100_000_000, currentBytes: 150_000_000, T0);

        var host = new ResilienceStatusPublisherHostedService(
            tracker, _factory, NullLogger<ResilienceStatusPublisherHostedService>.Instance);

        await host.PersistOnceAsync(CancellationToken.None);

        var row = await _readCtx.DriverInstanceResilienceStatuses.SingleAsync();
        row.BaselineFootprintBytes.ShouldBe(100_000_000);
        row.CurrentFootprintBytes.ShouldBe(150_000_000);
    }

    [Fact]
    public async Task TickCount_Advances_OnEveryCall()
    {
        var tracker = new DriverResilienceStatusTracker();
        var host = new ResilienceStatusPublisherHostedService(
            tracker, _factory, NullLogger<ResilienceStatusPublisherHostedService>.Instance);

        await host.PersistOnceAsync(CancellationToken.None);
        await host.PersistOnceAsync(CancellationToken.None);
        await host.PersistOnceAsync(CancellationToken.None);

        host.TickCount.ShouldBe(3);
    }
}
