using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Admin.Services;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;

/// <summary>
///     Ties Admin services end-to-end against a throwaway per-run database — mirrors the
///     Configuration fixture pattern. Spins up a fresh DB, applies migrations, exercises the
///     create-cluster → add-equipment → validate → publish → rollback happy path, then drops the
///     DB in Dispose. Confirms the stored procedures and managed validators agree with the UI
///     services.
/// </summary>
[Trait("Category", "Integration")]
public sealed class AdminServicesIntegrationTests : IDisposable
{
    private const string DefaultServer = "localhost,14330";
    private const string DefaultSaPassword = "OtOpcUaDev_2026!";

    private readonly string _databaseName = $"OtOpcUaAdminTest_{Guid.NewGuid():N}";
    private readonly string _connectionString;

    public AdminServicesIntegrationTests()
    {
        var server = Environment.GetEnvironmentVariable("OTOPCUA_CONFIG_TEST_SERVER") ?? DefaultServer;
        var password = Environment.GetEnvironmentVariable("OTOPCUA_CONFIG_TEST_SA_PASSWORD") ?? DefaultSaPassword;
        _connectionString =
            $"Server={server};Database={_databaseName};User Id=sa;Password={password};TrustServerCertificate=True;Encrypt=False;";

        using var ctx = NewContext();
        ctx.Database.Migrate();
    }

    public void Dispose()
    {
        using var conn = new Microsoft.Data.SqlClient.SqlConnection(
            new Microsoft.Data.SqlClient.SqlConnectionStringBuilder(_connectionString)
            { InitialCatalog = "master" }.ConnectionString);
        conn.Open();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = $@"
IF DB_ID(N'{_databaseName}') IS NOT NULL
BEGIN
    ALTER DATABASE [{_databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [{_databaseName}];
END";
        cmd.ExecuteNonQuery();
    }

    private OtOpcUaConfigDbContext NewContext()
    {
        var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
            .UseSqlServer(_connectionString)
            .Options;
        return new OtOpcUaConfigDbContext(opts);
    }

    [Fact]
    public async Task Create_cluster_add_equipment_validate_publish_roundtrips_the_full_admin_flow()
    {
        // 1. Create cluster + draft.
        await using (var ctx = NewContext())
        {
            var clusterSvc = new ClusterService(ctx);
            await clusterSvc.CreateAsync(new ServerCluster
            {
                ClusterId = "flow-1", Name = "Flow test", Enterprise = "zb", Site = "dev",
                NodeCount = 1, RedundancyMode = RedundancyMode.None, Enabled = true,
                CreatedBy = "test",
            }, createdBy: "test", CancellationToken.None);
        }

        long draftId;
        await using (var ctx = NewContext())
        {
            var genSvc = new GenerationService(ctx);
            var draft = await genSvc.CreateDraftAsync("flow-1", "test", CancellationToken.None);
            draftId = draft.GenerationId;
        }

        // 2. Add namespace + UNS + driver + equipment.
        await using (var ctx = NewContext())
        {
            var nsSvc = new NamespaceService(ctx);
            var unsSvc = new UnsService(ctx);
            var drvSvc = new DriverInstanceService(ctx);
            var eqSvc = new EquipmentService(ctx);

            var ns = await nsSvc.AddAsync(draftId, "flow-1", "urn:flow:ns", NamespaceKind.Equipment, CancellationToken.None);
            var area = await unsSvc.AddAreaAsync(draftId, "flow-1", "line-a", null, CancellationToken.None);
            var line = await unsSvc.AddLineAsync(draftId, area.UnsAreaId, "cell-1", null, CancellationToken.None);
            var driver = await drvSvc.AddAsync(draftId, "flow-1", ns.NamespaceId, "modbus", "ModbusTcp", "{}", CancellationToken.None);

            await eqSvc.CreateAsync(draftId, new Equipment
            {
                EquipmentUuid = Guid.NewGuid(),
                EquipmentId = string.Empty,
                DriverInstanceId = driver.DriverInstanceId,
                UnsLineId = line.UnsLineId,
                Name = "eq-1",
                MachineCode = "M001",
            }, CancellationToken.None);
        }

        // 3. Validate — should be error-free.
        await using (var ctx = NewContext())
        {
            var validationSvc = new DraftValidationService(ctx);
            var errors = await validationSvc.ValidateAsync(draftId, CancellationToken.None);
            errors.ShouldBeEmpty("draft with matched namespace/driver should validate clean");
        }

        // 4. Publish + verify status flipped.
        await using (var ctx = NewContext())
        {
            var genSvc = new GenerationService(ctx);
            await genSvc.PublishAsync("flow-1", draftId, "first publish", CancellationToken.None);
        }

        await using (var ctx = NewContext())
        {
            var status = await ctx.ConfigGenerations
                .Where(g => g.GenerationId == draftId)
                .Select(g => g.Status)
                .FirstAsync();
            status.ShouldBe(GenerationStatus.Published);
        }

        // 5. Rollback creates a new Published generation cloned from the target.
        await using (var ctx = NewContext())
        {
            var genSvc = new GenerationService(ctx);
            await genSvc.RollbackAsync("flow-1", draftId, "rollback test", CancellationToken.None);
        }

        await using (var ctx = NewContext())
        {
            var publishedCount = await ctx.ConfigGenerations
                .CountAsync(g => g.ClusterId == "flow-1" && g.Status == GenerationStatus.Published);
            publishedCount.ShouldBe(1, "rollback supersedes the prior publish with a new one");

            var supersededCount = await ctx.ConfigGenerations
                .CountAsync(g => g.ClusterId == "flow-1" && g.Status == GenerationStatus.Superseded);
            supersededCount.ShouldBeGreaterThanOrEqualTo(1);
        }
    }

    [Fact]
    public async Task Validate_draft_surfaces_cross_cluster_namespace_binding_violation()
    {
        await using (var ctx = NewContext())
        {
            await new ClusterService(ctx).CreateAsync(new ServerCluster
            {
                ClusterId = "c-A", Name = "A", Enterprise = "zb", Site = "dev",
                NodeCount = 1, RedundancyMode = RedundancyMode.None, Enabled = true, CreatedBy = "t",
            }, "t", CancellationToken.None);

            await new ClusterService(ctx).CreateAsync(new ServerCluster
            {
                ClusterId = "c-B", Name = "B", Enterprise = "zb", Site = "dev",
                NodeCount = 1, RedundancyMode = RedundancyMode.None, Enabled = true, CreatedBy = "t",
            }, "t", CancellationToken.None);
        }

        long draftId;
        await using (var ctx = NewContext())
        {
            var draft = await new GenerationService(ctx).CreateDraftAsync("c-A", "t", CancellationToken.None);
            draftId = draft.GenerationId;
        }

        await using (var ctx = NewContext())
        {
            // Namespace rooted in c-B, driver in c-A — decision #122 violation.
            var ns = await new NamespaceService(ctx)
                .AddAsync(draftId, "c-B", "urn:cross", NamespaceKind.Equipment, CancellationToken.None);
            await new DriverInstanceService(ctx)
                .AddAsync(draftId, "c-A", ns.NamespaceId, "drv", "ModbusTcp", "{}", CancellationToken.None);
        }

        await using (var ctx = NewContext())
        {
            var errors = await new DraftValidationService(ctx).ValidateAsync(draftId, CancellationToken.None);
            errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding");
        }
    }
}
