using System.Data.Common;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;

/// <summary>
/// Query-shape coverage for the Notification Outbox KPI snapshots (arch-review 04,
/// Task 20). Each snapshot must be a single-query conditional aggregation with a
/// bounded oldest-pending — the old shape issued 5-7 sequential COUNT round trips
/// and materialized the whole live queue to find the oldest row.
/// </summary>
public class NotificationOutboxRepositoryKpiQueryShapeTests
{
    private static Notification NewNotification(
        string sourceSiteId, string? sourceNode, NotificationStatus status,
        DateTimeOffset createdAt, DateTimeOffset? deliveredAt = null) =>
        new(Guid.NewGuid().ToString(), NotificationType.Email, "Ops List", "Subject", "Body", sourceSiteId)
        {
            Status = status,
            CreatedAt = createdAt,
            DeliveredAt = deliveredAt,
            SourceNode = sourceNode,
        };

    [Fact]
    public async Task AllThreeKpiSnapshots_IssueAtMostTwoQueries_AndBoundTheOldestLookup()
    {
        var counter = new SelectCountingInterceptor();
        await using var ctx = SqliteTestHelper.CreateInMemoryContext(counter);
        var now = DateTimeOffset.UtcNow;

        // Three non-terminal rows for one site/node — the "live queue" the old code
        // pulled into memory to find the oldest. All older than the stuck cutoff.
        ctx.Notifications.Add(NewNotification("plant-a", "node-a", NotificationStatus.Pending, now.AddMinutes(-30)));
        ctx.Notifications.Add(NewNotification("plant-a", "node-a", NotificationStatus.Retrying, now.AddMinutes(-20)));
        ctx.Notifications.Add(NewNotification("plant-a", "node-a", NotificationStatus.Pending, now.AddMinutes(-10)));
        await ctx.SaveChangesAsync();

        var repo = new NotificationOutboxRepository(ctx);
        var stuckCutoff = now.AddMinutes(-5);
        var deliveredSince = now.AddMinutes(-60);

        // Global: one aggregation query + one bounded oldest query.
        counter.Reset();
        var global = await repo.ComputeKpisAsync(stuckCutoff, deliveredSince);
        Assert.True(counter.Count <= 2,
            $"ComputeKpisAsync issued {counter.Count} queries against Notifications; expected <= 2");
        // The oldest lookup must be bounded (LIMIT/TOP), never a full non-terminal SELECT.
        Assert.Contains(counter.Commands, sql => sql.Contains("LIMIT", StringComparison.OrdinalIgnoreCase));
        // WP2.2: the aggregation must be PREDICATE-RESTRICTED, not an unrestricted
        // scan. Every KPI here concerns the live queue, the parked backlog or the
        // last delivery interval; historical Delivered/Discarded rows — which are
        // the overwhelming bulk of a retained table — contribute to none of them.
        // Without the WHERE the query's cost grows with retention rather than with
        // the working set. (An index-restricted aggregation was already the shape
        // of the per-site/per-node snapshots; the global one was the odd one out.)
        Assert.All(
            counter.Commands.Where(sql => sql.Contains("COUNT", StringComparison.OrdinalIgnoreCase)),
            sql => Assert.Contains("WHERE", sql, StringComparison.OrdinalIgnoreCase));
        Assert.Equal(3, global.QueueDepth);
        Assert.NotNull(global.OldestPendingAge);

        // Per-site: a single grouped conditional-aggregation query (counts + MIN).
        counter.Reset();
        var perSite = await repo.ComputePerSiteKpisAsync(stuckCutoff, deliveredSince);
        Assert.True(counter.Count <= 2,
            $"ComputePerSiteKpisAsync issued {counter.Count} queries against Notifications; expected <= 2");
        var siteA = Assert.Single(perSite);
        Assert.Equal(3, siteA.QueueDepth);
        Assert.NotNull(siteA.OldestPendingAge);

        // Per-node: same single grouped aggregation, keyed by SourceNode.
        counter.Reset();
        var perNode = await repo.ComputePerNodeKpisAsync(stuckCutoff, deliveredSince);
        Assert.True(counter.Count <= 2,
            $"ComputePerNodeKpisAsync issued {counter.Count} queries against Notifications; expected <= 2");
        var nodeA = Assert.Single(perNode);
        Assert.Equal(3, nodeA.QueueDepth);
        Assert.NotNull(nodeA.OldestPendingAge);
    }

    /// <summary>
    /// Records the text of every reader command touching the Notifications table so a
    /// test can count the round trips a KPI snapshot makes and inspect their shape.
    /// </summary>
    private sealed class SelectCountingInterceptor : DbCommandInterceptor
    {
        private readonly List<string> _commands = new();

        public IReadOnlyList<string> Commands => _commands;

        public int Count => _commands.Count;

        public void Reset() => _commands.Clear();

        private void Record(DbCommand command)
        {
            if (command.CommandText.Contains("Notifications", StringComparison.OrdinalIgnoreCase))
            {
                _commands.Add(command.CommandText);
            }
        }

        public override InterceptionResult<DbDataReader> ReaderExecuting(
            DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        {
            Record(command);
            return base.ReaderExecuting(command, eventData, result);
        }

        public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
            DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result,
            CancellationToken cancellationToken = default)
        {
            Record(command);
            return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
        }
    }
}
