using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
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;

/// <summary>
/// Integration tests for <see cref="SecuredWriteRepository"/> (M7 OPC UA / MxGateway
/// UX, Task T14b). Uses the same <see cref="MsSqlMigrationFixture"/> as the migration
/// tests so the EF reads/writes execute against the real <c>PendingSecuredWrites</c>
/// schema produced by the migration. Each test mints a fresh per-test
/// <c>SiteId</c>/<c>Status</c> suffix so tests neither collide nor require teardown.
/// Tests pair <see cref="SkippableFactAttribute"/> with <c>Skip.IfNot(...)</c> so the
/// runner reports them as Skipped (not Passed) when MSSQL is unreachable.
/// </summary>
public class SecuredWriteRepositoryTests : IClassFixture<MsSqlMigrationFixture>
{
    private readonly MsSqlMigrationFixture _fixture;

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

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

        var siteId = NewSiteId();

        // Add a Pending row.
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        var pending = NewRow(siteId, status: "Pending");
        var id = await repo.AddAsync(pending);
        Assert.True(id > 0, "AddAsync should return the store-generated identity.");

        // Get returns it.
        await using (var readContext = CreateContext())
        {
            var loaded = await new SecuredWriteRepository(readContext).GetAsync(id);
            Assert.NotNull(loaded);
            Assert.Equal(siteId, loaded!.SiteId);
            Assert.Equal("Pending", loaded.Status);
            Assert.Equal("conn-1", loaded.ConnectionName);
            Assert.Equal("Plant.Tank.Setpoint", loaded.TagPath);
            Assert.Equal("op.alice", loaded.OperatorUser);
            Assert.Null(loaded.VerifierUser);
            Assert.Null(loaded.DecidedAtUtc);
        }

        // Update to Approved.
        await using (var updateContext = CreateContext())
        {
            var updateRepo = new SecuredWriteRepository(updateContext);
            var toApprove = await updateRepo.GetAsync(id);
            Assert.NotNull(toApprove);
            toApprove!.Status = "Approved";
            toApprove.VerifierUser = "ver.bob";
            toApprove.VerifierComment = "looks good";
            toApprove.DecidedAtUtc = DateTime.UtcNow;
            await updateRepo.UpdateAsync(toApprove);
        }

        // Get reflects the update.
        await using (var verifyContext = CreateContext())
        {
            var reloaded = await new SecuredWriteRepository(verifyContext).GetAsync(id);
            Assert.NotNull(reloaded);
            Assert.Equal("Approved", reloaded!.Status);
            Assert.Equal("ver.bob", reloaded.VerifierUser);
            Assert.Equal("looks good", reloaded.VerifierComment);
            Assert.NotNull(reloaded.DecidedAtUtc);
        }

        // Query by status returns it.
        await using (var queryContext = CreateContext())
        {
            var queryRepo = new SecuredWriteRepository(queryContext);
            var byStatus = await queryRepo.QueryAsync(status: "Approved", siteId: siteId, skip: 0, take: 50);
            Assert.Contains(byStatus, p => p.Id == id);
            Assert.All(byStatus, p => Assert.Equal("Approved", p.Status));

            // A non-matching status filter excludes the row.
            var pendingPage = await queryRepo.QueryAsync(status: "Pending", siteId: siteId, skip: 0, take: 50);
            Assert.DoesNotContain(pendingPage, p => p.Id == id);
        }
    }

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

        var siteId = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        var pendingId = await repo.AddAsync(NewRow(siteId, status: "Pending"));
        var executedId = await repo.AddAsync(NewRow(siteId, status: "Executed"));

        var all = await repo.QueryAsync(status: null, siteId: siteId, skip: 0, take: 50);
        Assert.Contains(all, p => p.Id == pendingId);
        Assert.Contains(all, p => p.Id == executedId);
    }

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

        var siteId = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        var id = await repo.AddAsync(NewRow(siteId, status: "Pending"));

        Assert.True(await repo.TryMarkExpiredAsync(id, DateTime.UtcNow));
        // CAS: a second caller loses because the row is no longer Pending.
        Assert.False(await repo.TryMarkExpiredAsync(id, DateTime.UtcNow));

        await using var readContext = CreateContext();
        var row = await new SecuredWriteRepository(readContext).GetAsync(id);
        Assert.NotNull(row);
        Assert.Equal("Expired", row!.Status);
        Assert.NotNull(row.DecidedAtUtc);
        Assert.Null(row.VerifierUser); // system decision, no verifier
    }

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

        var siteId = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        var id = await repo.AddAsync(NewRow(siteId, status: "Pending"));
        Assert.True(await repo.TryMarkApprovedAsync(id, "ver.bob", null, DateTime.UtcNow));

        // Already Approved -> the conditional WHERE Status='Pending' matches no row.
        Assert.False(await repo.TryMarkExpiredAsync(id, DateTime.UtcNow));
    }

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

        var siteId = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        await repo.AddAsync(NewRow(siteId, status: "Pending"));
        await repo.AddAsync(NewRow(siteId, status: "Pending"));

        Assert.Equal(2, await repo.CountAsync(status: "Pending", siteId: siteId));
        Assert.Equal(0, await repo.CountAsync(status: "Executed", siteId: siteId));
    }

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

        // Distinct per-run site identifiers stand in for "SITE1"/"SITE2"/"SITE3" so the
        // permitted-set filter is asserted deterministically against a shared MSSQL.
        var site1 = NewSiteId();
        var site2 = NewSiteId();
        var site3 = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        await repo.AddAsync(NewRow(site1, status: "Pending"));
        await repo.AddAsync(NewRow(site1, status: "Pending"));
        await repo.AddAsync(NewRow(site2, status: "Pending"));
        await repo.AddAsync(NewRow(site3, status: "Pending"));

        var rows = await repo.QueryAsync(status: null, siteId: null, skip: 0, take: 100,
            permittedSiteIds: new[] { site1, site3 });
        Assert.Equal(3, rows.Count);
        Assert.DoesNotContain(rows, r => r.SiteId == site2);
    }

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

        var site1 = NewSiteId();
        var site2 = NewSiteId();
        var site3 = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        await repo.AddAsync(NewRow(site1, status: "Pending"));
        await repo.AddAsync(NewRow(site1, status: "Pending"));
        await repo.AddAsync(NewRow(site2, status: "Pending"));
        await repo.AddAsync(NewRow(site3, status: "Pending"));

        var count = await repo.CountAsync(status: null, siteId: null,
            permittedSiteIds: new[] { site1, site3 });
        Assert.Equal(3, count);
    }

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

        // 4 rows on a single unique site; null permittedSiteIds must not filter them
        // out — exactly today's behavior.
        var siteId = NewSiteId();
        await using var context = CreateContext();
        var repo = new SecuredWriteRepository(context);

        await repo.AddAsync(NewRow(siteId, status: "Pending"));
        await repo.AddAsync(NewRow(siteId, status: "Pending"));
        await repo.AddAsync(NewRow(siteId, status: "Executed"));
        await repo.AddAsync(NewRow(siteId, status: "Rejected"));

        var rows = await repo.QueryAsync(status: null, siteId: siteId, skip: 0, take: 100,
            permittedSiteIds: null);
        Assert.Equal(4, rows.Count);
    }

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

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

    private static string NewSiteId() =>
        "site-t14b-" + Guid.NewGuid().ToString("N").Substring(0, 8);

    private static PendingSecuredWrite NewRow(string siteId, string status) => new()
    {
        SiteId = siteId,
        ConnectionName = "conn-1",
        TagPath = "Plant.Tank.Setpoint",
        ValueJson = "42.5",
        ValueType = "Double",
        Status = status,
        OperatorUser = "op.alice",
        OperatorComment = "raise setpoint",
        SubmittedAtUtc = DateTime.UtcNow,
    };
}
