using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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.Enums;
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.Commons.Types.Templates;

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

/// <summary>
/// FU-B — integration coverage for the post-flush second-pass rewire in
/// <c>BundleImporter.ApplyAsync</c>: composition edges (<c>#39</c>) and
/// alarm-script FKs (remainder of <c>#37</c>). All three scenarios drive the
/// full export → load → apply pipeline so the wire-level DTO carries the
/// name-keyed references the importer is expected to resolve.
/// </summary>
public sealed class CompositionImportTests : IDisposable
{
    private readonly ServiceProvider _provider;

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

        var dbName = $"CompositionImportTests_{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();

    /// <summary>
    /// Builds a bundle of the templates currently in the DB and returns the
    /// raw bytes. Mirrors the helper in <c>RoundTripTests</c> but parameterised
    /// to keep the per-test setup terse.
    /// </summary>
    private async Task<byte[]> ExportAllTemplatesAsync()
    {
        await using var scope = _provider.CreateAsyncScope();
        var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
        var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
        var templateIds = await ctx.Templates.Select(t => t.Id).ToListAsync();

        var selection = new ExportSelection(
            TemplateIds: templateIds,
            SharedScriptIds: Array.Empty<int>(),
            ExternalSystemIds: Array.Empty<int>(),
            DatabaseConnectionIds: Array.Empty<int>(),
            NotificationListIds: Array.Empty<int>(),
            SmtpConfigurationIds: Array.Empty<int>(),
            ApiMethodIds: Array.Empty<int>(),
            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);
        return ms.ToArray();
    }

    private async Task WipeTemplatesAsync()
    {
        await using var scope = _provider.CreateAsyncScope();
        var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
        ctx.TemplateCompositions.RemoveRange(ctx.TemplateCompositions);
        ctx.TemplateAlarms.RemoveRange(ctx.TemplateAlarms);
        ctx.TemplateScripts.RemoveRange(ctx.TemplateScripts);
        ctx.Templates.RemoveRange(ctx.Templates);
        ctx.AuditLogEntries.RemoveRange(ctx.AuditLogEntries);
        await ctx.SaveChangesAsync();
    }

    [Fact]
    public async Task Composition_edges_are_restored_on_import()
    {
        // Seed: Template A composes Template B via InstanceName="b1".
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("B") { Description = "leaf" });
            await ctx.SaveChangesAsync();
            var b = await ctx.Templates.SingleAsync(t => t.Name == "B");

            var a = new Template("A") { Description = "composer" };
            ctx.Templates.Add(a);
            await ctx.SaveChangesAsync();
            a.Compositions.Add(new TemplateComposition("b1")
            {
                TemplateId = a.Id,
                ComposedTemplateId = b.Id,
            });
            await ctx.SaveChangesAsync();
        }

        var bundleBytes = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

        // Load + preview + apply with Add for both.
        Guid sessionId;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            using var ms = new MemoryStream(bundleBytes, writable: false);
            var session = await importer.LoadAsync(ms, passphrase: null);
            sessionId = session.SessionId;
        }

        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            preview = await importer.PreviewAsync(sessionId);
        }
        var resolutions = preview.Items
            .Select(i => new ImportResolution(i.EntityType, i.Name, ResolutionAction.Add, null))
            .ToList();

        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            await importer.ApplyAsync(sessionId, resolutions, user: "bob");
        }

        // Assert: A has exactly one TemplateComposition pointing at B.
        await using (var assertScope = _provider.CreateAsyncScope())
        {
            var ctx = assertScope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var a = await ctx.Templates
                .Include(t => t.Compositions)
                .SingleAsync(t => t.Name == "A");
            var comp = Assert.Single(a.Compositions);
            Assert.Equal("b1", comp.InstanceName);

            var b = await ctx.Templates.SingleAsync(t => t.Name == "B");
            Assert.Equal(b.Id, comp.ComposedTemplateId);
        }
    }

    [Fact]
    public async Task Composition_referencing_skipped_template_emits_unresolved_audit_and_skips_edge()
    {
        // Same seed as the happy-path test; the divergence is in the Apply
        // resolutions — B is Skip-resolved so its composition reference is
        // expected to surface as a BundleImportCompositionUnresolved audit row
        // and the composition edge must NOT be written.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("B") { Description = "leaf" });
            await ctx.SaveChangesAsync();
            var b = await ctx.Templates.SingleAsync(t => t.Name == "B");

            var a = new Template("A") { Description = "composer" };
            ctx.Templates.Add(a);
            await ctx.SaveChangesAsync();
            a.Compositions.Add(new TemplateComposition("b1")
            {
                TemplateId = a.Id,
                ComposedTemplateId = b.Id,
            });
            await ctx.SaveChangesAsync();
        }

        var bundleBytes = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

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

        ImportResult result;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            // Add A but Skip B. The composition's ComposedTemplateName="B"
            // therefore can't resolve (B isn't being written and isn't in the
            // target — we wiped) and must surface as an unresolved audit row.
            var resolutions = new List<ImportResolution>
            {
                new("Template", "A", ResolutionAction.Add, null),
                new("Template", "B", ResolutionAction.Skip, null),
            };
            result = await importer.ApplyAsync(sessionId, resolutions, user: "bob");
        }

        await using (var assertScope = _provider.CreateAsyncScope())
        {
            var ctx = assertScope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var a = await ctx.Templates
                .Include(t => t.Compositions)
                .SingleAsync(t => t.Name == "A");
            // Composition couldn't resolve → zero edges on A.
            Assert.Empty(a.Compositions);

            // B was skipped → still not in target.
            Assert.False(await ctx.Templates.AnyAsync(t => t.Name == "B"));

            // BundleImportCompositionUnresolved audit row exists and is
            // correlated by the run's BundleImportId.
            var unresolved = await ctx.AuditLogEntries
                .Where(e => e.Action == "BundleImportCompositionUnresolved")
                .ToListAsync();
            var row = Assert.Single(unresolved);
            Assert.Equal(result.BundleImportId, row.BundleImportId);
            Assert.Equal("TemplateComposition", row.EntityType);
            // Entity name is "<owner>.<instance>" so an operator can find the
            // offending row in the Configuration Audit Log Viewer quickly.
            Assert.Equal("A.b1", row.EntityName);
        }
    }

    [Fact]
    public async Task OnTriggerScript_alarm_link_is_restored_on_import()
    {
        // Seed: a template with a "Startup" script and an alarm whose
        // OnTriggerScriptId points at "Startup". The DTO carries
        // OnTriggerScriptName="Startup" and the importer's second pass should
        // re-resolve the FK to Startup's new id.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = new Template("Pump") { Description = "with alarm" };
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();

            var script = new TemplateScript("Startup", "return 1;")
            {
                TemplateId = t.Id,
            };
            t.Scripts.Add(script);
            await ctx.SaveChangesAsync();

            t.Alarms.Add(new TemplateAlarm("High")
            {
                TemplateId = t.Id,
                PriorityLevel = 2,
                TriggerType = AlarmTriggerType.RangeViolation,
                OnTriggerScriptId = script.Id,
            });
            await ctx.SaveChangesAsync();
        }

        var bundleBytes = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

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

        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var preview = await importer.PreviewAsync(sessionId);
            var resolutions = preview.Items
                .Select(i => new ImportResolution(i.EntityType, i.Name, ResolutionAction.Add, null))
                .ToList();
            await importer.ApplyAsync(sessionId, resolutions, user: "bob");
        }

        await using (var assertScope = _provider.CreateAsyncScope())
        {
            var ctx = assertScope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var pump = await ctx.Templates
                .Include(t => t.Alarms)
                .Include(t => t.Scripts)
                .SingleAsync(t => t.Name == "Pump");

            var script = Assert.Single(pump.Scripts);
            Assert.Equal("Startup", script.Name);

            var alarm = Assert.Single(pump.Alarms);
            Assert.Equal("High", alarm.Name);
            // FK rewired to the imported Startup script's NEW id (not the
            // pre-export id, which is gone after the wipe).
            Assert.Equal(script.Id, alarm.OnTriggerScriptId);
        }
    }
}
