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.Transport.Import;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;

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

/// <summary>
/// C3 — the import apply path must wire template inheritance edges
/// (<c>ParentTemplateId</c>) from the bundle's name-keyed <c>BaseTemplateName</c>.
/// Before the fix a derived template imported as a root (null parent) — silent
/// data loss. Mirrors <c>CompositionImportTests</c>: full export → load →
/// preview → apply, so the wire-level DTO carries the reference to resolve.
/// </summary>
public sealed class InheritanceImportTests : IDisposable
{
    private readonly ServiceProvider _provider;

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

        var dbName = $"InheritanceImportTests_{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>();
        services.AddScoped<ISiteRepository, SiteRepository>();
        services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
        services.AddScoped<IAuditService, AuditService>();
        services.AddTransport();

        _provider = services.BuildServiceProvider();
    }

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

    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.Templates.RemoveRange(ctx.Templates);
        ctx.AuditLogEntries.RemoveRange(ctx.AuditLogEntries);
        await ctx.SaveChangesAsync();
    }

    /// <summary>Seed base "Pump" + derived "WaterPump" (ParentTemplateId -> Pump).</summary>
    private async Task SeedBaseAndDerivedAsync()
    {
        await using var scope = _provider.CreateAsyncScope();
        var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
        var pump = new Template("Pump") { Description = "base" };
        ctx.Templates.Add(pump);
        await ctx.SaveChangesAsync();
        ctx.Templates.Add(new Template("WaterPump") { Description = "derived", ParentTemplateId = pump.Id });
        await ctx.SaveChangesAsync();
    }

    private async Task<Guid> LoadAsync(byte[] bundleBytes)
    {
        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);
        return session.SessionId;
    }

    [Fact]
    public async Task Import_DerivedTemplate_WiresParentTemplateId()
    {
        await SeedBaseAndDerivedAsync();
        var bundle = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

        var sessionId = await LoadAsync(bundle);
        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 assert = _provider.CreateAsyncScope())
        {
            var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var basePump = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            var derived = await ctx.Templates.SingleAsync(t => t.Name == "WaterPump");
            Assert.Equal(basePump.Id, derived.ParentTemplateId); // FAILS today: null
        }
    }

    [Fact]
    public async Task Import_Overwrite_UpdatesParentEdge_AndRenamedBaseIsHonoured()
    {
        // Bundle base renamed to "Pump2" via ResolutionAction.Rename; derived's
        // BaseTemplateName ("Pump") must resolve through the rename map to Pump2.
        await SeedBaseAndDerivedAsync();
        var bundle = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

        var sessionId = await LoadAsync(bundle);
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var resolutions = new List<ImportResolution>
            {
                new("Template", "Pump", ResolutionAction.Rename, "Pump2"),
                new("Template", "WaterPump", ResolutionAction.Add, null),
            };
            await importer.ApplyAsync(sessionId, resolutions, user: "bob");
        }

        await using (var assert = _provider.CreateAsyncScope())
        {
            var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var pump2 = await ctx.Templates.SingleAsync(t => t.Name == "Pump2");
            var derived = await ctx.Templates.SingleAsync(t => t.Name == "WaterPump");
            Assert.Equal(pump2.Id, derived.ParentTemplateId);
        }
    }

    [Fact]
    public async Task Import_DerivedWithSkippedBase_LeavesNullAndAuditsUnresolved()
    {
        // Base "Pump" is Skip-resolved and absent from the wiped target, so the
        // derived's BaseTemplateName cannot resolve — the edge stays null and a
        // BundleImportBaseTemplateUnresolved audit row is emitted (never throws).
        await SeedBaseAndDerivedAsync();
        var bundle = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

        var sessionId = await LoadAsync(bundle);
        ImportResult result;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var resolutions = new List<ImportResolution>
            {
                new("Template", "Pump", ResolutionAction.Skip, null),
                new("Template", "WaterPump", ResolutionAction.Add, null),
            };
            result = await importer.ApplyAsync(sessionId, resolutions, user: "bob");
        }

        await using (var assert = _provider.CreateAsyncScope())
        {
            var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var derived = await ctx.Templates.SingleAsync(t => t.Name == "WaterPump");
            Assert.Null(derived.ParentTemplateId);
            Assert.False(await ctx.Templates.AnyAsync(t => t.Name == "Pump"));

            var row = Assert.Single(await ctx.AuditLogEntries
                .Where(e => e.Action == "BundleImportBaseTemplateUnresolved")
                .ToListAsync());
            Assert.Equal(result.BundleImportId, row.BundleImportId);
            Assert.Equal("Template", row.EntityType);
            Assert.Equal("WaterPump", row.EntityName);
        }
    }

    /// <summary>Seed base "root" + derived "leaf" (leaf ParentTemplateId -> root).</summary>
    private async Task SeedParentChildAsync(string rootName, string leafName)
    {
        await using var scope = _provider.CreateAsyncScope();
        var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
        var root = new Template(rootName) { Description = "root" };
        ctx.Templates.Add(root);
        await ctx.SaveChangesAsync();
        ctx.Templates.Add(new Template(leafName) { Description = "leaf", ParentTemplateId = root.Id });
        await ctx.SaveChangesAsync();
    }

    [Fact]
    public async Task Import_Overwrite_ThatClosesInheritanceCycle_Throws_AndRollsBack()
    {
        // Source: base "Alpha" (root) + derived "Beta" (parent Alpha), so the
        // exported Beta DTO carries BaseTemplateName = "Alpha".
        await SeedParentChildAsync("Alpha", "Beta");
        var bundle = await ExportAllTemplatesAsync();
        await WipeTemplatesAsync();

        // Target: the MIRROR — "Beta" (root) + "Alpha" (parent Beta). Overwriting
        // Beta so it inherits from Alpha closes Alpha -> Beta -> Alpha.
        await SeedParentChildAsync("Beta", "Alpha");

        var sessionId = await LoadAsync(bundle);
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var resolutions = new List<ImportResolution>
            {
                new("Template", "Alpha", ResolutionAction.Skip, null),
                new("Template", "Beta", ResolutionAction.Overwrite, null),
            };
            var ex = await Assert.ThrowsAsync<SemanticValidationException>(
                () => importer.ApplyAsync(sessionId, resolutions, user: "bob"));
            Assert.Contains("cycle", ex.Message, StringComparison.OrdinalIgnoreCase);
        }

        // Rolled back: target Beta's parent edge is untouched (still a root).
        await using (var assert = _provider.CreateAsyncScope())
        {
            var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var beta = await ctx.Templates.SingleAsync(t => t.Name == "Beta");
            Assert.Null(beta.ParentTemplateId);
        }
    }

    [Fact]
    public async Task Import_Overwrite_ThatClosesCompositionCycle_Throws_AndRollsBack()
    {
        // Source: leaf "Gamma" + "Delta" that composes Gamma, so the exported
        // Delta DTO carries a composition edge to "Gamma".
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Gamma") { Description = "leaf" });
            await ctx.SaveChangesAsync();
            var gamma = await ctx.Templates.SingleAsync(t => t.Name == "Gamma");
            var delta = new Template("Delta") { Description = "composer" };
            ctx.Templates.Add(delta);
            await ctx.SaveChangesAsync();
            delta.Compositions.Add(new TemplateComposition("g1")
            {
                TemplateId = delta.Id,
                ComposedTemplateId = gamma.Id,
            });
            await ctx.SaveChangesAsync();
        }

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

        // Target: the MIRROR — "Gamma" composes "Delta". Overwriting Delta so it
        // composes Gamma closes the composition loop Gamma -> Delta -> Gamma.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Delta") { Description = "target-leaf" });
            await ctx.SaveChangesAsync();
            var delta = await ctx.Templates.SingleAsync(t => t.Name == "Delta");
            var gamma = new Template("Gamma") { Description = "target-composer" };
            ctx.Templates.Add(gamma);
            await ctx.SaveChangesAsync();
            gamma.Compositions.Add(new TemplateComposition("d1")
            {
                TemplateId = gamma.Id,
                ComposedTemplateId = delta.Id,
            });
            await ctx.SaveChangesAsync();
        }

        var sessionId = await LoadAsync(bundle);
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var resolutions = new List<ImportResolution>
            {
                new("Template", "Gamma", ResolutionAction.Skip, null),
                new("Template", "Delta", ResolutionAction.Overwrite, null),
            };
            var ex = await Assert.ThrowsAsync<SemanticValidationException>(
                () => importer.ApplyAsync(sessionId, resolutions, user: "bob"));
            Assert.Contains("cycle", ex.Message, StringComparison.OrdinalIgnoreCase);
        }

        // Rolled back: target Delta still has NO composition rows (Overwrite did
        // not persist the loop-closing edge).
        await using (var assert = _provider.CreateAsyncScope())
        {
            var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var delta = await ctx.Templates
                .Include(t => t.Compositions)
                .SingleAsync(t => t.Name == "Delta");
            Assert.Empty(delta.Compositions);
        }
    }
}
