using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
using Xunit;

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

/// <summary>
/// CD-015 race-fix integration tests for
/// <see cref="NotificationOutboxRepository.InsertIfNotExistsAsync"/>. The method
/// is raw-SQL (<c>IF NOT EXISTS … INSERT</c>) matching the AuditLog and SiteCalls
/// idempotent-insert pattern; it must execute against a real SQL Server schema,
/// so this class uses <see cref="MsSqlMigrationFixture"/> rather than the SQLite
/// in-memory provider used by <see cref="RepositoryCoverageTests"/>.
/// </summary>
public class NotificationOutboxRepositoryIntegrationTests : IClassFixture<MsSqlMigrationFixture>
{
    private readonly MsSqlMigrationFixture _fixture;

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

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

        var id = Guid.NewGuid().ToString();
        await using var context = CreateContext();
        var repo = new NotificationOutboxRepository(context);

        var inserted = await repo.InsertIfNotExistsAsync(MakeNotification(id));

        Assert.True(inserted);

        await using var readContext = CreateContext();
        var loaded = await readContext.Notifications.FindAsync(id);
        Assert.NotNull(loaded);
        Assert.Equal("Subject", loaded!.Subject);
    }

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

        var id = Guid.NewGuid().ToString();
        await using (var context = CreateContext())
        {
            var repo = new NotificationOutboxRepository(context);
            await repo.InsertIfNotExistsAsync(MakeNotification(id, subject: "Original"));
        }

        await using (var context = CreateContext())
        {
            var repo = new NotificationOutboxRepository(context);
            var inserted = await repo.InsertIfNotExistsAsync(MakeNotification(id, subject: "Changed"));
            Assert.False(inserted);
        }

        await using var readContext = CreateContext();
        var loaded = await readContext.Notifications.FindAsync(id);
        Assert.NotNull(loaded);
        Assert.Equal("Original", loaded!.Subject);
    }

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

        // CD-015 race coverage. The IF NOT EXISTS … INSERT pattern has a
        // check-then-act window: two concurrent sessions can both pass the
        // EXISTS check and both attempt the INSERT — the loser surfaces as a
        // SqlException with Number 2601/2627. The site→central handoff is
        // documented at-least-once with insert-if-not-exists, so this collision
        // IS the expected contention mode. The race losers MUST be swallowed
        // (not bubbled) so the site doesn't retry the same NotificationId
        // forever. Final row count must be exactly 1; no exceptions thrown.
        var id = Guid.NewGuid().ToString();

        await Parallel.ForEachAsync(
            Enumerable.Range(0, 50),
            new ParallelOptions { MaxDegreeOfParallelism = 50 },
            async (_, ct) =>
            {
                await using var context = CreateContext();
                var repo = new NotificationOutboxRepository(context);
                await repo.InsertIfNotExistsAsync(MakeNotification(id), ct);
            });

        await using var readContext = CreateContext();
        var count = await readContext.Notifications
            .Where(n => n.NotificationId == id)
            .CountAsync();
        Assert.Equal(1, count);
    }

    // --- helpers ------------------------------------------------------------

    private ScadaBridgeDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlServer(_fixture.ConnectionString)
            .Options;
        return new ScadaBridgeDbContext(options);
    }

    private static Notification MakeNotification(
        string id,
        NotificationStatus status = NotificationStatus.Pending,
        string subject = "Subject")
    {
        return new Notification(
            id,
            NotificationType.Email,
            "Ops List",
            subject,
            "Body",
            "site-cd015")
        {
            Status = status,
            CreatedAt = new DateTimeOffset(2026, 5, 20, 10, 0, 0, TimeSpan.Zero),
            SiteEnqueuedAt = new DateTimeOffset(2026, 5, 20, 9, 59, 0, TimeSpan.Zero),
        };
    }
}
