using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
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;

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

/// <summary>
/// Integration tests for <see cref="ZB.MOM.WW.ScadaBridge.Transport.Import.BundleImporter.PreviewAsync"/>.
/// Reuses the same in-memory host pattern as the exporter tests: real
/// repositories, real EF in-memory provider, real Transport pipeline. Each test
/// seeds the target DB, exports a bundle, then loads + previews it via the
/// importer.
/// </summary>
public sealed class BundleImporterPreviewTests : IDisposable
{
    private readonly ServiceProvider _provider;

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

        var dbName = $"BundleImporterPreviewTests_{Guid.NewGuid()}";
        services.AddDbContext<ScadaBridgeDbContext>(opts => opts.UseInMemoryDatabase(dbName));

        services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
        services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
        services.AddScoped<INotificationRepository, NotificationRepository>();
        services.AddScoped<IInboundApiRepository, InboundApiRepository>();
        services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
        services.AddScoped<IAuditService, AuditService>();
        services.AddTransport();

        _provider = services.BuildServiceProvider();
    }

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

    private async Task<Stream> ExportTemplatesAsync()
    {
        await using var scope = _provider.CreateAsyncScope();
        var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
        var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
        var ids = await ctx.Templates.Select(t => t.Id).ToListAsync();
        var selection = new ExportSelection(
            TemplateIds: ids,
            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);

        return await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
            passphrase: null, cancellationToken: CancellationToken.None);
    }

    private static async Task<byte[]> StreamToBytes(Stream s)
    {
        using var ms = new MemoryStream();
        await s.CopyToAsync(ms);
        return ms.ToArray();
    }

    [Fact]
    public async Task PreviewAsync_classifies_artifact_as_Identical_when_fields_match()
    {
        // Arrange: seed a template, export it, leave target unchanged. The
        // bundle's DTO is the literal projection of the target, so the diff
        // should classify it as Identical.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Pump") { Description = "stable" });
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        // Act
        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Assert
        var pumpItem = Assert.Single(preview.Items, i => i.EntityType == "Template" && i.Name == "Pump");
        Assert.Equal(ConflictKind.Identical, pumpItem.Kind);
        Assert.Null(pumpItem.FieldDiffJson);
    }

    [Fact]
    public async Task PreviewAsync_classifies_artifact_as_Modified_with_field_diff()
    {
        // Arrange: seed a template with Description="new", export it, then
        // overwrite the target template's Description with "old". The bundle's
        // version differs from the target, so the diff should flag the
        // Description field.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Pump") { Description = "new" });
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        // Mutate the target between export and preview so the diff has
        // something to report. The bundle still carries Description="new".
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = await ctx.Templates.SingleAsync(x => x.Name == "Pump");
            t.Description = "old";
            await ctx.SaveChangesAsync();
        }

        // Act
        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Assert
        var pumpItem = Assert.Single(preview.Items, i => i.EntityType == "Template" && i.Name == "Pump");
        Assert.Equal(ConflictKind.Modified, pumpItem.Kind);
        Assert.NotNull(pumpItem.FieldDiffJson);
        // The diff should mention the Description field by name.
        Assert.Contains("Description", pumpItem.FieldDiffJson!, StringComparison.Ordinal);
    }

    [Fact]
    public async Task PreviewAsync_classifies_artifact_as_New_when_absent_from_target()
    {
        // Arrange: seed a template, export it, then delete it from the target
        // database. The bundle still contains the template, so the diff should
        // classify it as New (target is now empty).
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Pump") { Description = "to-be-deleted" });
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = await ctx.Templates.SingleAsync();
            ctx.Templates.Remove(t);
            await ctx.SaveChangesAsync();
        }

        // Act
        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Assert
        var pumpItem = Assert.Single(preview.Items, i => i.EntityType == "Template" && i.Name == "Pump");
        Assert.Equal(ConflictKind.New, pumpItem.Kind);
        Assert.Null(pumpItem.FieldDiffJson);
    }

    [Fact]
    public async Task PreviewAsync_emits_Blocker_when_required_dependency_missing()
    {
        // Arrange: seed a template whose script body calls MissingHelper(), and
        // an unrelated HelperFn() shared script that *is* defined but isn't the
        // referenced one. We then export WITHOUT IncludeDependencies and use a
        // selection that only pulls the template — the bundle won't carry
        // MissingHelper (it doesn't exist anywhere) so the preview must flag it.
        //
        // To get MissingHelper into the bundle script body without the export
        // resolver pulling it in (it can't — it doesn't exist), we just seed
        // the template with a script that mentions it; the resolver scan only
        // matters for entity discovery, the body text is preserved verbatim.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.SharedScripts.Add(new SharedScript("HelperFn", "return 1;"));
            ctx.ExternalSystemDefinitions.Add(new ExternalSystemDefinition("ErpSystem", "https://erp.example", "ApiKey"));

            var t = new Template("Pump") { Description = "broken" };
            t.Scripts.Add(new TemplateScript("init", "var x = MissingHelper();"));
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        // Wipe the SharedScripts table so MissingHelper has no chance of being
        // resolved in the target either. (HelperFn is intentionally seeded so
        // we can verify the blocker check is specific — it should NOT flag
        // HelperFn since it's in the target.)
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            // Keep HelperFn + ErpSystem so they're in the target's resolved set.
            // Just confirm via assertion that MissingHelper is the blocker name.
            await ctx.SaveChangesAsync();
        }

        // Act
        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Assert: there's at least one Blocker, and the MissingHelper one is in there.
        Assert.Contains(preview.Items, i => i.Kind == ConflictKind.Blocker);
        Assert.Contains(preview.Items, i =>
            i.Kind == ConflictKind.Blocker
            && i.Name == "MissingHelper"
            && i.BlockerReason is not null
            && i.BlockerReason.Contains("MissingHelper", StringComparison.Ordinal));
        // Conversely, HelperFn must NOT be a blocker — it's seeded in the target.
        Assert.DoesNotContain(preview.Items, i =>
            i.Kind == ConflictKind.Blocker && i.Name == "HelperFn");
    }

    [Fact]
    public async Task PreviewAsync_does_not_flag_opcua_tag_paths_in_DataSourceReference_as_blockers()
    {
        // Arrange: a template with an attribute whose DataSourceReference is an
        // OPC UA node-address path -- e.g. "ns=3;s=Tank.Level". The segment
        // before the dot ("Tank") used to be parsed by the blocker heuristic as
        // a potential SharedScript reference, even though tag paths live in the
        // device's address space and are not script-callable.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = new Template("Pump") { Description = "tag-path-check" };
            t.Attributes.Add(new TemplateAttribute("Level")
            {
                Value = "0",
                DataSourceReference = "ns=3;s=Tank.Level",
            });
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        // Act
        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Assert: "Tank" (the device-owned tag-path root segment) must not be
        // flagged as a missing SharedScript or ExternalSystem reference.
        Assert.DoesNotContain(preview.Items, i =>
            i.Kind == ConflictKind.Blocker && i.Name == "Tank");
    }

    [Fact]
    public async Task PreviewAsync_does_not_flag_stdlib_or_runtime_member_accesses_as_blockers()
    {
        // Arrange: a template script that uses a representative mix of stdlib
        // calls, runtime-API roots, and member-access patterns. None of these
        // are user-defined SharedScripts or ExternalSystems and the previous
        // heuristic was flagging every one of them.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = new Template("Pump") { Description = "noise-check" };
            t.Scripts.Add(new TemplateScript("init", """
                var now = DateTimeOffset.UtcNow;
                var s = Convert.ToString(123);
                await Notify.Send("alerts", "msg");
                var x = await Database.ExecuteScalarAsync("SELECT COUNT(*) FROM t");
                var y = await ExternalSystem.Call("erp", "ping");
                obj.Dispose();
                """));
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        // Act
        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Assert: none of the well-known names produce blocker rows.
        string[] noiseNames =
        {
            "DateTimeOffset", "UtcNow", "Convert", "ToString", "Notify", "Send",
            "Database", "ExecuteScalarAsync", "COUNT", "ExternalSystem", "Call",
            "Dispose",
        };
        foreach (var name in noiseNames)
        {
            Assert.DoesNotContain(preview.Items, i =>
                i.Kind == ConflictKind.Blocker && i.Name == name);
        }
    }

    [Fact]
    public async Task PreviewAsync_multiple_templates_with_children_diffs_each_correctly()
    {
        // Transport-008 regression: PreviewAsync previously fetched each matching
        // template's children via a per-name GetTemplateWithChildrenAsync call
        // (N+1). The bulk variant returns every match in a single query — this
        // test seeds three templates with distinct child collections and asserts
        // the preview hydrates each one so the per-child diff sees the right
        // attribute / alarm / script counts (i.e. the bulk fetch did not lose
        // any child rows compared to the per-name fetch).
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var pump = new Template("Pump") { Description = "p1" };
            pump.Attributes.Add(new TemplateAttribute("Flow"));
            pump.Scripts.Add(new TemplateScript("init", "return 1;"));
            var valve = new Template("Valve") { Description = "v1" };
            valve.Alarms.Add(new TemplateAlarm("HighPressure"));
            var tank = new Template("Tank") { Description = "t1" };
            tank.Attributes.Add(new TemplateAttribute("Level"));
            tank.Attributes.Add(new TemplateAttribute("Temperature"));
            ctx.Templates.AddRange(pump, valve, tank);
            await ctx.SaveChangesAsync();
        }

        var bundleStream = await ExportTemplatesAsync();
        var bytes = await StreamToBytes(bundleStream);

        ImportPreview preview;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
            preview = await importer.PreviewAsync(session.SessionId);
        }

        // Each template should be diff-classified (Identical, since the bundle
        // is the literal projection of the target). Critically, the diff must
        // succeed for ALL three — a bulk-fetch bug that silently drops rows
        // would surface here as a missing item or a wrong (New) classification.
        var pumpItem = Assert.Single(preview.Items, i => i.EntityType == "Template" && i.Name == "Pump");
        var valveItem = Assert.Single(preview.Items, i => i.EntityType == "Template" && i.Name == "Valve");
        var tankItem = Assert.Single(preview.Items, i => i.EntityType == "Template" && i.Name == "Tank");
        Assert.Equal(ConflictKind.Identical, pumpItem.Kind);
        Assert.Equal(ConflictKind.Identical, valveItem.Kind);
        Assert.Equal(ConflictKind.Identical, tankItem.Kind);
    }
}
