using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;

/// <summary>
/// Bundle F (#23 M6-T12) end-to-end tests for the
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service running
/// the real EF/MSSQL <see cref="AuditLogPartitionMaintenance"/> against the
/// per-class <see cref="MsSqlMigrationFixture"/>. The migration seeds
/// boundaries for every month Jan 2026 – Dec 2027, so the eager startup tick
/// can be exercised both for the "future covered" no-op case and for the
/// "lookahead larger than covered" SPLIT case.
/// </summary>
/// <remarks>
/// Tests within this class share one fixture DB — boundaries added by one
/// test persist across the next. Each test reads the max boundary at the
/// start and computes its lookahead relative to it, mirroring the pattern
/// used by the per-component <c>AuditLogPartitionMaintenanceTests</c> in
/// <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests</c>.
/// </remarks>
public class PartitionMaintenanceTests : IClassFixture<MsSqlMigrationFixture>
{
    private readonly MsSqlMigrationFixture _fixture;

    public PartitionMaintenanceTests(MsSqlMigrationFixture fixture)
    {
        _fixture = fixture;
    }

    private ScadaBridgeDbContext CreateContext() =>
        new(new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlServer(_fixture.ConnectionString).Options);

    /// <summary>
    /// Builds the central-side DI graph for the hosted service: scoped EF
    /// context + scoped <see cref="IPartitionMaintenance"/> matching how
    /// <c>AddConfigurationDatabase</c> wires the production composition root.
    /// </summary>
    private ServiceProvider BuildProvider()
    {
        var services = new ServiceCollection();
        services.AddDbContext<ScadaBridgeDbContext>(
            opts => opts.UseSqlServer(_fixture.ConnectionString),
            ServiceLifetime.Scoped);
        services.AddScoped<IPartitionMaintenance, AuditLogPartitionMaintenance>();
        return services.BuildServiceProvider();
    }

    private static async Task<DateTime?> ReadMaxBoundaryAsync(IServiceProvider sp)
    {
        await using var scope = sp.CreateAsyncScope();
        var maintenance = scope.ServiceProvider.GetRequiredService<IPartitionMaintenance>();
        return await maintenance.GetMaxBoundaryAsync();
    }

    /// <summary>
    /// Mirrors the helper in
    /// <c>AuditLogPartitionMaintenanceTests.LookaheadForExtraBoundaries</c>:
    /// the smallest lookahead value that lands the SPLIT horizon exactly
    /// <paramref name="extraBoundaries"/> months past the current max.
    /// </summary>
    private static int LookaheadForExtraBoundaries(DateTime max, int extraBoundaries)
    {
        var nowFirstOfNextMonth = FirstOfNextMonth(DateTime.UtcNow);
        var monthsToMax = ((max.Year - nowFirstOfNextMonth.Year) * 12)
            + max.Month - nowFirstOfNextMonth.Month;
        return monthsToMax + extraBoundaries;
    }

    private static int LookaheadInsideExistingRange(DateTime max)
    {
        var now = DateTime.UtcNow;
        var months = ((max.Year - now.Year) * 12) + max.Month - now.Month - 1;
        return Math.Max(1, months);
    }

    private static DateTime FirstOfNextMonth(DateTime instant)
    {
        var firstOfThisMonth = new DateTime(instant.Year, instant.Month, 1, 0, 0, 0, DateTimeKind.Utc);
        return firstOfThisMonth.AddMonths(1);
    }

    /// <summary>
    /// Awaits one full tick of the hosted service. The service runs an
    /// eager startup tick inside <see cref="AuditLogPartitionMaintenanceService.StartAsync"/>'s
    /// continuation, but the continuation is dispatched on a background
    /// Task.Run — so we poll the side effect (the boundary count or
    /// max-boundary value) until it changes.
    /// </summary>
    private async Task StartAndAwaitStartupTickAsync(
        AuditLogPartitionMaintenanceService svc,
        Func<Task<bool>> awaitCondition,
        TimeSpan timeout)
    {
        await svc.StartAsync(CancellationToken.None);
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (await awaitCondition())
            {
                return;
            }
            await Task.Delay(50);
        }
    }

    // ---------------------------------------------------------------------
    // 1. EndToEnd_DefaultLookahead_NoSplit_WhenFutureCovered
    // ---------------------------------------------------------------------

    [SkippableFact]
    public async Task EndToEnd_DefaultLookahead_NoSplit_WhenFutureCovered()
    {
        Skip.IfNot(_fixture.Available, _fixture.SkipReason);

        await using var sp = BuildProvider();

        // The migration seeds boundaries through Dec 2027. With default
        // lookahead = 1 and today = ~2026-05-20, horizon =
        // NormalizeToFirstOfMonth(now) + 1 = 2026-07-01, well within the
        // seeded range, so the startup tick should issue zero SPLITs.
        var maxBefore = await ReadMaxBoundaryAsync(sp);
        Assert.NotNull(maxBefore);

        // Skip if the fixture DB already has boundaries past Dec 2027 from
        // a prior test in this class — the lookahead-already-covered path
        // is what we want to exercise, regardless of how far past Dec 2027
        // the boundary may be.
        var opts = Options.Create(new AuditLogPartitionMaintenanceOptions
        {
            IntervalSeconds = 60, // long enough that only the startup tick fires inside the test window
            LookaheadMonths = 1,
        });

        var svc = new AuditLogPartitionMaintenanceService(
            sp.GetRequiredService<IServiceScopeFactory>(),
            opts,
            NullLogger<AuditLogPartitionMaintenanceService>.Instance);

        // Drive the startup tick. There is no public completion handle;
        // poll until either (a) the max boundary changes (which would be a
        // failure for this test) or (b) the polling window expires (success).
        await svc.StartAsync(CancellationToken.None);
        await Task.Delay(TimeSpan.FromSeconds(2));
        await svc.StopAsync(CancellationToken.None);
        svc.Dispose();

        // Assert the max boundary is unchanged: no SPLIT was issued.
        var maxAfter = await ReadMaxBoundaryAsync(sp);
        Assert.Equal(maxBefore, maxAfter);
    }

    // ---------------------------------------------------------------------
    // 2. EndToEnd_LookaheadLargerThanCovered_Splits_NewBoundaries
    // ---------------------------------------------------------------------

    [SkippableFact]
    public async Task EndToEnd_LookaheadLargerThanCovered_Splits_NewBoundaries()
    {
        Skip.IfNot(_fixture.Available, _fixture.SkipReason);

        await using var sp = BuildProvider();

        var maxBefore = await ReadMaxBoundaryAsync(sp);
        Assert.NotNull(maxBefore);

        // Pick a lookahead that adds exactly two new boundaries past the
        // current max. The expected new boundaries are max+1mo and max+2mo.
        var lookahead = LookaheadForExtraBoundaries(maxBefore.Value, extraBoundaries: 2);
        var expectedFirstNew = maxBefore.Value.AddMonths(1);
        var expectedSecondNew = maxBefore.Value.AddMonths(2);

        var opts = Options.Create(new AuditLogPartitionMaintenanceOptions
        {
            IntervalSeconds = 60,
            LookaheadMonths = lookahead,
        });

        var svc = new AuditLogPartitionMaintenanceService(
            sp.GetRequiredService<IServiceScopeFactory>(),
            opts,
            NullLogger<AuditLogPartitionMaintenanceService>.Instance);

        // Drive the startup tick. Wait until max boundary moves forward by
        // the expected amount; SPLIT against MSSQL can take a second or two
        // on a busy dev container.
        await StartAndAwaitStartupTickAsync(
            svc,
            async () =>
            {
                var current = await ReadMaxBoundaryAsync(sp);
                return current == expectedSecondNew;
            },
            timeout: TimeSpan.FromSeconds(15));

        await svc.StopAsync(CancellationToken.None);
        svc.Dispose();

        var maxAfter = await ReadMaxBoundaryAsync(sp);
        // Two new boundaries should be present after the startup tick. The
        // hosted service does not surface the added-list directly (it logs
        // only at Information), so we assert via the max-boundary delta.
        Assert.Equal(expectedSecondNew, maxAfter);
        // Sanity: the intermediate boundary was also added (the loop
        // SPLITs every month from max+1 up to horizon, in order).
        Assert.True(expectedFirstNew < expectedSecondNew);
    }

    // ---------------------------------------------------------------------
    // 3. EndToEnd_PartitionMaintenance_Idempotent_OverTwoRuns
    // ---------------------------------------------------------------------

    [SkippableFact]
    public async Task EndToEnd_PartitionMaintenance_Idempotent_OverTwoRuns()
    {
        Skip.IfNot(_fixture.Available, _fixture.SkipReason);

        await using var sp = BuildProvider();

        var maxBefore = await ReadMaxBoundaryAsync(sp);
        Assert.NotNull(maxBefore);

        // Add exactly one new boundary on the first run.
        var lookahead = LookaheadForExtraBoundaries(maxBefore.Value, extraBoundaries: 1);
        var expectedAdded = maxBefore.Value.AddMonths(1);

        var opts = Options.Create(new AuditLogPartitionMaintenanceOptions
        {
            IntervalSeconds = 60,
            LookaheadMonths = lookahead,
        });

        // First run.
        var svc1 = new AuditLogPartitionMaintenanceService(
            sp.GetRequiredService<IServiceScopeFactory>(),
            opts,
            NullLogger<AuditLogPartitionMaintenanceService>.Instance);
        await StartAndAwaitStartupTickAsync(
            svc1,
            async () =>
            {
                var current = await ReadMaxBoundaryAsync(sp);
                return current == expectedAdded;
            },
            timeout: TimeSpan.FromSeconds(15));
        await svc1.StopAsync(CancellationToken.None);
        svc1.Dispose();

        var maxAfterFirst = await ReadMaxBoundaryAsync(sp);
        Assert.Equal(expectedAdded, maxAfterFirst);

        // Second run with the SAME lookahead value. Because the boundary
        // is already covered, the EnsureLookaheadAsync call must be a
        // no-op — max boundary is unchanged AND no exception is thrown.
        var svc2 = new AuditLogPartitionMaintenanceService(
            sp.GetRequiredService<IServiceScopeFactory>(),
            opts,
            NullLogger<AuditLogPartitionMaintenanceService>.Instance);
        await svc2.StartAsync(CancellationToken.None);
        // Wait long enough that the startup tick would have fired and
        // logged any boundary addition; the boundary state must remain
        // unchanged after the wait.
        await Task.Delay(TimeSpan.FromSeconds(2));
        await svc2.StopAsync(CancellationToken.None);
        svc2.Dispose();

        var maxAfterSecond = await ReadMaxBoundaryAsync(sp);
        Assert.Equal(maxAfterFirst, maxAfterSecond);
    }
}
