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

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

/// <summary>
///     Round-trips <see cref="ClusterNode.AkkaPort"/> / <see cref="ClusterNode.GrpcPort"/> through
///     the real schema (per-cluster mesh Phase 1). These are <b>central's dial targets</b>: Phase 2
///     builds its ClusterClient contact points from <c>Host</c> + <c>AkkaPort</c>, and Phase 5 dials
///     <c>GrpcPort</c> for the telemetry stream. Central cannot see a site node's appsettings once
///     the meshes split, so the value has to live in a row central can read.
/// </summary>
[Trait("Category", "SchemaCompliance")]
[Collection(nameof(SchemaComplianceCollection))]
public sealed class ClusterNodeTransportPortsTests(SchemaComplianceFixture fixture)
{
    /// <summary>Verifies both ports survive a DB round-trip and that AkkaPort defaults to 4053.</summary>
    [Fact]
    public async Task Transport_ports_round_trip_and_AkkaPort_defaults_to_4053()
    {
        await using var ctx = NewContext();
        var clusterId = await SeedClusterAsync(ctx);

        // Explicit non-default values on one node — proves the columns are mapped and persisted
        // rather than silently dropped (an unmapped property round-trips fine in memory).
        ctx.ClusterNodes.Add(NewNode(clusterId, "explicit", n =>
        {
            n.AkkaPort = 4055;
            n.GrpcPort = 5223;
        }));
        // Nothing set on the other — AkkaPort must land on 4053 and GrpcPort must stay null.
        // GrpcPort is deliberately nullable: nothing listens on it until Phase 5, and a non-null
        // default would assert a port that does not exist.
        ctx.ClusterNodes.Add(NewNode(clusterId, "defaulted"));
        await ctx.SaveChangesAsync();

        await using var read = NewContext();
        var explicitNode = await read.ClusterNodes.AsNoTracking()
            .SingleAsync(n => n.NodeId == $"{clusterId}-explicit:4053");
        explicitNode.AkkaPort.ShouldBe(4055);
        explicitNode.GrpcPort.ShouldBe(5223);

        var defaulted = await read.ClusterNodes.AsNoTracking()
            .SingleAsync(n => n.NodeId == $"{clusterId}-defaulted:4053");
        defaulted.AkkaPort.ShouldBe(4053);
        defaulted.GrpcPort.ShouldBeNull();
    }

    /// <summary>
    ///     Verifies the <b>DB-side</b> default on <c>AkkaPort</c> — the migration's
    ///     <c>defaultValue: 4053</c>, which is what rows created before the column existed inherit.
    ///     The entity round-trip above cannot see this: <see cref="ClusterNode.AkkaPort"/>'s CLR
    ///     initializer is also 4053, so that assertion passes with the mapping default deleted. This
    ///     one inserts through raw SQL with the column omitted, so only the schema can supply the value.
    /// </summary>
    [Fact]
    public async Task AkkaPort_has_a_DB_side_default_of_4053()
    {
        await using var ctx = NewContext();
        var clusterId = await SeedClusterAsync(ctx);

        await using (var conn = fixture.OpenConnection())
        {
            var cmd = conn.CreateCommand();
            cmd.CommandText = """
                INSERT INTO dbo.ClusterNode (NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
                                             ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
                VALUES (@nodeId, @clusterId, @host, 4840, 8081, @uri, 200, 1, 'raw-sql');
                """;
            cmd.Parameters.AddWithValue("@nodeId", $"{clusterId}-raw:4053");
            cmd.Parameters.AddWithValue("@clusterId", clusterId);
            cmd.Parameters.AddWithValue("@host", $"{clusterId}-raw");
            cmd.Parameters.AddWithValue("@uri", $"urn:OtOpcUa:{clusterId}-raw");
            await cmd.ExecuteNonQueryAsync();
        }

        await using var read = NewContext();
        var row = await read.ClusterNodes.AsNoTracking()
            .SingleAsync(n => n.NodeId == $"{clusterId}-raw:4053");
        row.AkkaPort.ShouldBe(4053);
        row.GrpcPort.ShouldBeNull();
    }

    /// <summary>
    ///     Verifies the columns are queryable server-side — Phase 2's contact-point refresh selects
    ///     them in SQL rather than materialising every node, so a client-side-evaluation regression
    ///     (or an unmapped property) has to fail here.
    /// </summary>
    [Fact]
    public async Task Transport_ports_are_queryable_server_side()
    {
        await using var ctx = NewContext();
        var clusterId = await SeedClusterAsync(ctx);
        ctx.ClusterNodes.Add(NewNode(clusterId, "a", n => n.GrpcPort = 5223));
        ctx.ClusterNodes.Add(NewNode(clusterId, "b"));
        await ctx.SaveChangesAsync();

        await using var read = NewContext();
        var contacts = await read.ClusterNodes.AsNoTracking()
            .Where(n => n.ClusterId == clusterId && n.GrpcPort != null)
            .Select(n => new { n.Host, n.AkkaPort, n.GrpcPort })
            .ToListAsync();

        contacts.Count.ShouldBe(1);
        contacts[0].AkkaPort.ShouldBe(4053);
        contacts[0].GrpcPort.ShouldBe(5223);
    }

    private static ClusterNode NewNode(string clusterId, string suffix, Action<ClusterNode>? configure = null)
    {
        var node = new ClusterNode
        {
            NodeId = $"{clusterId}-{suffix}:4053",
            ClusterId = clusterId,
            Host = $"{clusterId}-{suffix}",
            ApplicationUri = $"urn:OtOpcUa:{clusterId}-{suffix}",
            CreatedBy = nameof(ClusterNodeTransportPortsTests),
        };
        configure?.Invoke(node);
        return node;
    }

    private static async Task<string> SeedClusterAsync(OtOpcUaConfigDbContext ctx)
    {
        // Unique per test: the collection shares one database, and ApplicationUri is fleet-wide unique.
        var clusterId = $"PORTS-{Guid.NewGuid():N}"[..24];
        ctx.ServerClusters.Add(new ServerCluster
        {
            ClusterId = clusterId,
            Name = clusterId,
            Enterprise = "zb",
            Site = "ports-test",
            // CK_ServerCluster_RedundancyMode_NodeCount: Warm/Hot require exactly 2 nodes.
            RedundancyMode = RedundancyMode.Warm,
            NodeCount = 2,
            CreatedBy = nameof(ClusterNodeTransportPortsTests),
        });
        await ctx.SaveChangesAsync();
        return clusterId;
    }

    private OtOpcUaConfigDbContext NewContext() =>
        new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
            .UseSqlServer(fixture.ConnectionString)
            .Options);
}
