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.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>
/// T26 — integration conflict-resolution tests. The unit-level Apply paths
/// in <c>BundleImporterApplyTests</c> exercise hand-built sessions; this
/// suite drives the full export→load→apply pipeline so the wire-level
/// session round-trip is part of the assertion.
/// </summary>
public sealed class ConflictResolutionTests : IDisposable
{
    private readonly ServiceProvider _provider;

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

        var dbName = $"ConflictResolutionTests_{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>
    /// Exports the current set of templates+shared scripts to a freshly built
    /// bundle and immediately loads it into a session. Returns the
    /// <see cref="BundleSession.SessionId"/> the caller can hand to ApplyAsync.
    /// </summary>
    private async Task<Guid> ExportAndLoadAsync()
    {
        byte[] bundleBytes;
        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 sharedScriptIds = await ctx.SharedScripts.Select(s => s.Id).ToListAsync();

            var selection = new ExportSelection(
                TemplateIds: templateIds,
                SharedScriptIds: sharedScriptIds,
                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();
        }

        await using var loadScope = _provider.CreateAsyncScope();
        var importer = loadScope.ServiceProvider.GetRequiredService<IBundleImporter>();
        using var input = new MemoryStream(bundleBytes, writable: false);
        var session = await importer.LoadAsync(input, passphrase: null);
        return session.SessionId;
    }

    [Fact]
    public async Task Overwrite_replaces_existing_template_description()
    {
        // Arrange: seed Pump with Description="new", export it (the bundle
        // therefore carries "new"), then mutate the target's Pump to "old".
        // Apply with Overwrite must restore "new".
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Pump") { Description = "new" });
            await ctx.SaveChangesAsync();
        }

        var sessionId = await ExportAndLoadAsync();

        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var existing = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            existing.Description = "old";
            await ctx.SaveChangesAsync();
        }

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

        // Assert
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var pump = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            Assert.Equal("new", pump.Description);
        }
        Assert.Equal(1, result.Overwritten);
    }

    [Fact]
    public async Task Skip_leaves_existing_template_unchanged()
    {
        // Arrange: seed Pump with Description="keep", export, then mutate to
        // "replace" so the bundle's body diverges. Skip must NOT touch the
        // target row, and the summary must report Skipped=1.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Pump") { Description = "replace" });
            await ctx.SaveChangesAsync();
        }
        var sessionId = await ExportAndLoadAsync();
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var existing = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            existing.Description = "keep";
            await ctx.SaveChangesAsync();
        }

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

        // Assert
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var pump = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            Assert.Equal("keep", pump.Description);
        }
        Assert.Equal(1, result.Skipped);
        Assert.Equal(0, result.Added);
        Assert.Equal(0, result.Overwritten);
    }

    [Fact]
    public async Task Rename_creates_new_template_alongside_existing()
    {
        // Arrange: seed Pump, export, mutate description so the rename target
        // is obviously the bundle's version. The original Pump must survive
        // untouched and a second Pump.Imported template must materialise.
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            ctx.Templates.Add(new Template("Pump") { Description = "from-bundle" });
            await ctx.SaveChangesAsync();
        }
        var sessionId = await ExportAndLoadAsync();
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var existing = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            existing.Description = "kept-original";
            await ctx.SaveChangesAsync();
        }

        // Act
        ImportResult result;
        await using (var scope = _provider.CreateAsyncScope())
        {
            var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
            result = await importer.ApplyAsync(sessionId,
                new List<ImportResolution>
                {
                    new("Template", "Pump", ResolutionAction.Rename, "Pump.Imported"),
                },
                user: "bob");
        }

        // Assert
        await using (var scope = _provider.CreateAsyncScope())
        {
            var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
            var original = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
            Assert.Equal("kept-original", original.Description);

            var renamed = await ctx.Templates.SingleAsync(t => t.Name == "Pump.Imported");
            Assert.Equal("from-bundle", renamed.Description);
        }
        Assert.Equal(1, result.Renamed);
    }
}
