using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
using ZB.MOM.WW.ScadaBridge.Transport;
using ZB.MOM.WW.ScadaBridge.Transport.Import;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;

namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests;

/// <summary>
/// T26 — integration validation-failure rollback test. Complements the unit
/// rollback covered by <c>BundleImporterApplyTests</c> by driving the full
/// export → load → apply path, so the failing validation runs against a
/// real export-side serialised bundle.
/// </summary>
public sealed class ValidationFailureTests : IDisposable
{
    private readonly ServiceProvider _provider;

    public ValidationFailureTests()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(
            new ConfigurationBuilder().AddInMemoryCollection().Build());

        var dbName = $"ValidationFailureTests_{Guid.NewGuid()}";
        services.AddDbContext<ScadaBridgeDbContext>(opts => opts
            .UseInMemoryDatabase(dbName)
            .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));

        services.AddSingleton<ITemplateGraphWatermark, TemplateGraphWatermark>();
        services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
        services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
        services.AddScoped<INotificationRepository, NotificationRepository>();
        services.AddScoped<IInboundApiRepository, InboundApiRepository>();
        // M8: DependencyResolver now injects ISiteRepository to walk the
        // site/data-connection/instance closure; register it or activation fails.
        services.AddScoped<ISiteRepository, SiteRepository>();
        services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
        services.AddScoped<IAuditService, AuditService>();
        services.AddTransport();

        _provider = services.BuildServiceProvider();
    }

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

    [Fact]
    public async Task Semantic_validation_failure_rolls_back_all_writes()
    {
        // Arrange:
        //   - Seed an ApiMethod whose script body references MissingHelper(),
        //     a SharedScript that is NOT in the bundle (we don't seed one) and
        //     also NOT in the target after the wipe. ApiMethod name-resolution
        //     findings are HARD errors (no downstream deploy gate re-validates
        //     them — unlike template-script findings, which are advisory as of
        //     #05-T19), so this is the vehicle that still forces a rollback.
        //   - Export the ApiMethod only (no helper alongside).
        //   - Wipe the target so the apply runs through Add-paths.
        //   - Snapshot row counts so the rollback assertion is unambiguous.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.ApiMethods.Add(new ApiMethod("BrokenIngest", "var x = MissingHelper();"));
            await ctx.SaveChangesAsync();
        }

        // Export only the broken ApiMethod. IncludeDependencies=false guarantees
        // MissingHelper is NOT pulled in even if one happened to exist.
        byte[] bundleBytes;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var ids = await ctx.ApiMethods.Select(m => m.Id).ToListAsync();
            var selection = new ExportSelection(
                TemplateIds: Array.Empty<int>(),
                SharedScriptIds: Array.Empty<int>(),
                ExternalSystemIds: Array.Empty<int>(),
                DatabaseConnectionIds: Array.Empty<int>(),
                NotificationListIds: Array.Empty<int>(),
                SmtpConfigurationIds: Array.Empty<int>(),
                ApiMethodIds: ids,
                IncludeDependencies: false);

            var stream = await exporter.ExportAsync(selection,
                user: "alice", sourceEnvironment: "dev",
                passphrase: null, cancellationToken: CancellationToken.None);
            using var ms = new MemoryStream();
            await stream.CopyToAsync(ms);
            bundleBytes = ms.ToArray();
        }

        // Wipe target so the apply attempts an Add (the validation failure must
        // not let it land).
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.ApiMethods.RemoveRange(ctx.ApiMethods);
            ctx.SharedScripts.RemoveRange(ctx.SharedScripts);
            await ctx.SaveChangesAsync();
        }

        int apiMethodsBefore;
        int sharedScriptsBefore;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            apiMethodsBefore = await ctx.ApiMethods.CountAsync();
            sharedScriptsBefore = await ctx.SharedScripts.CountAsync();
        }
        Assert.Equal(0, apiMethodsBefore);
        Assert.Equal(0, sharedScriptsBefore);

        // Load the bundle into a session.
        Guid sessionId;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            using var input = new MemoryStream(bundleBytes, writable: false);
            var session = await importer.LoadAsync(input, passphrase: null);
            sessionId = session.SessionId;
        }

        // Act — apply with Add. Validation MUST throw.
        SemanticValidationException ex = default!;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            ex = await Assert.ThrowsAsync<SemanticValidationException>(() =>
                importer.ApplyAsync(sessionId,
                    new List<ImportResolution>
                    {
                        new("ApiMethod", "BrokenIngest", ResolutionAction.Add, null),
                    },
                    user: "bob"));
        }

        // Assert
        // 1. Errors list calls out the missing dependency by name.
        Assert.NotEmpty(ex.Errors);
        Assert.Contains(ex.Errors,
            err => err.Contains("MissingHelper", StringComparison.Ordinal));

        // 2. No new ApiMethod or SharedScript row was committed.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            Assert.Equal(apiMethodsBefore, await ctx.ApiMethods.CountAsync());
            Assert.Equal(sharedScriptsBefore, await ctx.SharedScripts.CountAsync());

            // 3. A BundleImportFailed audit row exists. The BundleImporter
            //    contract (T17) is: failure row's BundleImportId is NULL (the
            //    rolled-back id is intentionally disowned) and its
            //    AfterStateJson.Reason calls out the validation failure.
            var failure = await ctx.AuditLogEntries
                .SingleAsync(a => a.Action == "BundleImportFailed");
            Assert.Equal("Bundle", failure.EntityType);
            Assert.Null(failure.BundleImportId);
            Assert.NotNull(failure.AfterStateJson);
            // The exception message lands in the Reason field of the payload
            // (BundleImporter.ApplyAsync wires it through). Spot-check for
            // "validation" so the row's correlation to the failure is visible.
            Assert.Contains("validation", failure.AfterStateJson!,
                StringComparison.OrdinalIgnoreCase);
        }
    }
}
