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;

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

/// <summary>
/// FU-C — integration tests for the two-tier semantic validation wired into
/// <see cref="BundleImporter.ApplyAsync"/>: Pass 1 is the minimal name-
/// resolution scan (carried forward from the v1 importer) and Pass 2 is the
/// full <c>SemanticValidator</c> over each imported template's
/// <c>FlattenedConfiguration</c>. Pass 1 fails fast — Pass 2 only runs when
/// Pass 1 succeeds — so the Pass 2 scenarios here are chosen to live entirely
/// in alarm shape (alarm JSON is not scanned by Pass 1).
/// <para>
/// The "invalid call target" test exercises Pass 1 because every
/// SemanticValidator call-target rule presupposes the called identifier is
/// already known to the script body's surface; an entirely-unknown identifier
/// surfaces at Pass 1 first by design. Both tiers throw the same
/// <see cref="SemanticValidationException"/> with errors propagated.
/// </para>
/// </summary>
public sealed class SemanticValidatorImportTests : IDisposable
{
    private readonly ServiceProvider _provider;

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

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

        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();

    /// <summary>
    /// Export everything currently seeded, wipe the DB, then LoadAsync the
    /// bundle. Returns the session id. Mirrors the helper in
    /// <c>BundleImporterApplyTests</c> but exported as a free helper so each
    /// test can seed its own template shape without sharing fixture state.
    /// </summary>
    private async Task<Guid> ExportWipeAndLoadAsync()
    {
        byte[] bundleBytes;
        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);

            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 so the apply is exercising the Add path.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.TemplateAlarms.RemoveRange(ctx.TemplateAlarms);
            ctx.TemplateScripts.RemoveRange(ctx.TemplateScripts);
            ctx.TemplateAttributes.RemoveRange(ctx.TemplateAttributes);
            ctx.Templates.RemoveRange(ctx.Templates);
            await ctx.SaveChangesAsync();
        }

        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;
        }
        return sessionId;
    }

    [Fact]
    public async Task SemanticValidator_catches_invalid_call_target_at_import()
    {
        // Arrange — template whose script body calls UnknownHelper(): a
        // PascalCase identifier that doesn't resolve to any SharedScript or
        // ExternalSystem in the bundle or the target. This is the operator-
        // facing "invalid call target" surface — the full SemanticValidator's
        // CallScript/CallShared signature checks live downstream of name
        // resolution (you can't check arg count against a function that
        // doesn't exist). Pass 1 catches it first and fails fast.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = new Template("ScriptCallsUnknown");
            t.Scripts.Add(new Commons.Entities.Templates.TemplateScript(
                "init",
                "var x = UnknownHelper();"));
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();
        }

        var sessionId = await ExportWipeAndLoadAsync();

        // Act — apply must throw SemanticValidationException carrying the bad
        // call target by name.
        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("Template", "ScriptCallsUnknown", ResolutionAction.Add, null),
                    },
                    user: "bob"));
        }

        // Assert — error message names the bad target.
        Assert.NotEmpty(ex.Errors);
        Assert.Contains(ex.Errors,
            err => err.Contains("UnknownHelper", StringComparison.Ordinal));

        // Rollback — no template row landed.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            Assert.False(await ctx.Templates.AnyAsync(t => t.Name == "ScriptCallsUnknown"));
        }
    }

    [Fact]
    public async Task SemanticValidator_catches_alarm_trigger_type_mismatch_at_import()
    {
        // Arrange — template with a String attribute Status and a
        // RangeViolation alarm against it. The full SemanticValidator must
        // report TriggerOperandType (RangeViolation requires numeric).
        // Pass 1 doesn't scan alarm JSON, so the error reaches Pass 2.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = new Template("TankWithBadAlarm") { Description = "RangeViolation on string attr" };
            t.Attributes.Add(new TemplateAttribute("Status")
            {
                DataType = DataType.String,
                Value = "OK",
            });
            t.Alarms.Add(new TemplateAlarm("BadRange")
            {
                TriggerType = AlarmTriggerType.RangeViolation,
                TriggerConfiguration = "{\"attributeName\":\"Status\",\"min\":0,\"max\":100}",
                PriorityLevel = 1,
            });
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();
        }

        var sessionId = await ExportWipeAndLoadAsync();

        // Act
        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("Template", "TankWithBadAlarm", ResolutionAction.Add, null),
                    },
                    user: "bob"));
        }

        // Assert — error names the offending alarm and the bad trigger
        // type so the operator can locate the fix.
        Assert.NotEmpty(ex.Errors);
        Assert.Contains(ex.Errors,
            err => err.Contains("BadRange", StringComparison.Ordinal)
                && err.Contains("RangeViolation", StringComparison.Ordinal));

        // Rollback — no template row landed.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            Assert.False(await ctx.Templates.AnyAsync(t => t.Name == "TankWithBadAlarm"));
        }
    }

    [Fact]
    public async Task Valid_bundle_passes_semantic_validation()
    {
        // Arrange — clean template that satisfies both passes: one Double
        // attribute, one ValueMatch alarm on it, one script with no external
        // call identifiers. ValueMatch doesn't constrain the operand data
        // type (only RangeViolation / HiLo do), so this template's alarm is
        // legal.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var t = new Template("CleanPump") { Description = "passes both passes" };
            t.Attributes.Add(new TemplateAttribute("Speed")
            {
                DataType = DataType.Double,
                Value = "0",
            });
            t.Alarms.Add(new TemplateAlarm("Overspeed")
            {
                TriggerType = AlarmTriggerType.ValueMatch,
                TriggerConfiguration = "{\"attributeName\":\"Speed\",\"value\":100}",
                PriorityLevel = 1,
            });
            t.Scripts.Add(new Commons.Entities.Templates.TemplateScript(
                "tick",
                "// no external calls"));
            ctx.Templates.Add(t);
            await ctx.SaveChangesAsync();
        }

        var sessionId = await ExportWipeAndLoadAsync();

        // Act — happy-path import.
        ImportResult result;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            result = await importer.ApplyAsync(sessionId,
                new List<ImportResolution>
                {
                    new("Template", "CleanPump", ResolutionAction.Add, null),
                },
                user: "bob");
        }

        // Assert — template + alarm survived the round-trip.
        Assert.Equal(1, result.Added);
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            Assert.True(await ctx.Templates.AnyAsync(t => t.Name == "CleanPump"));
            Assert.True(await ctx.TemplateAlarms.AnyAsync(a => a.Name == "Overspeed"));
        }
    }
}
