using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Fleet;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;

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

/// <summary>
///     Proves the reconciler is actually wired to something — subscribes to membership, reads the DB,
///     and emits. <see cref="ClusterNodeAddressReconcilerTests"/> covers the comparison logic, which a
///     dormant actor would leave perfectly correct and completely inert.
/// </summary>
public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTestBase
{
    /// <summary>
    ///     An enabled row with no matching driver member is warned about. The harness ActorSystem
    ///     joins as <c>admin</c> with no driver members, so the seeded row is unmatched by
    ///     construction.
    /// </summary>
    [Fact]
    public void Enabled_row_with_no_matching_member_is_logged()
    {
        var dbFactory = NewInMemoryDbFactory();
        SeedNode(dbFactory, "site-a-1:4053", enabled: true);

        EventFilter.Warning(contains: "site-a-1:4053").ExpectOne(TimeSpan.FromSeconds(10), () =>
            Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory)));
    }

    /// <summary>
    ///     The finding set is logged once, not once per sweep. A check that reprints the same warning
    ///     every five minutes trains operators to filter it out, which costs more than it catches —
    ///     so this runs a deliberately fast sweep and asserts the count stays at one.
    /// </summary>
    [Fact]
    public void Unchanged_findings_are_not_repeated_on_every_sweep()
    {
        var dbFactory = NewInMemoryDbFactory();
        SeedNode(dbFactory, "site-a-1:4053", enabled: true);

        // ~6 sweeps inside the window; without the change-detection guard this logs 6 times.
        EventFilter.Warning(contains: "site-a-1:4053").Expect(1, TimeSpan.FromSeconds(3), () =>
        {
            Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(
                dbFactory, sweepInterval: TimeSpan.FromMilliseconds(400)));
            // Hold the filter open past several sweeps rather than returning immediately.
            ExpectNoMsg(TimeSpan.FromSeconds(2.5));
        });
    }

    /// <summary>
    ///     A disabled row is silence — the maintenance hatch must not trade a failed deployment for a
    ///     permanent warning. Positive control for the test above: same setup, one flag flipped.
    /// </summary>
    [Fact]
    public void Disabled_row_produces_no_warning()
    {
        var dbFactory = NewInMemoryDbFactory();
        SeedNode(dbFactory, "site-a-1:4053", enabled: false);

        EventFilter.Warning(contains: "site-a-1:4053").Expect(0, TimeSpan.FromSeconds(2), () =>
        {
            Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(
                dbFactory, sweepInterval: TimeSpan.FromMilliseconds(400)));
            ExpectNoMsg(TimeSpan.FromSeconds(1.5));
        });
    }

    /// <summary>
    ///     Per-cluster mesh Phase 6: an admin node scoped to its OWN cluster ignores rows in a cluster
    ///     its mesh can no longer see. Scoped to <c>MAIN</c>, a <c>SITE-A</c> enabled row is filtered
    ///     out before <see cref="ClusterNodeAddressReconciler.Reconcile"/> ever sees it, so it produces
    ///     no <see cref="AddressMismatchKind.EnabledRowNotInCluster"/> warning. This is the false
    ///     positive the split would otherwise print forever.
    /// </summary>
    [Fact]
    public void Row_in_a_foreign_cluster_is_ignored_when_scoped()
    {
        var dbFactory = NewInMemoryDbFactory();
        SeedNode(dbFactory, "site-a-1:4053", enabled: true, clusterId: "SITE-A");

        EventFilter.Warning(contains: "site-a-1:4053").Expect(0, TimeSpan.FromSeconds(2), () =>
        {
            Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(
                dbFactory, ownClusterId: "MAIN", sweepInterval: TimeSpan.FromMilliseconds(400)));
            ExpectNoMsg(TimeSpan.FromSeconds(1.5));
        });
    }

    /// <summary>
    ///     Scoping does not silence the admin node's own cluster. Scoped to <c>MAIN</c>, an enabled
    ///     <c>MAIN</c> row with no matching driver member is still warned about — proving the filter
    ///     keeps in-cluster rows (and with them, within-cluster drift detection).
    /// </summary>
    [Fact]
    public void Enabled_row_in_own_cluster_is_still_logged_when_scoped()
    {
        var dbFactory = NewInMemoryDbFactory();
        SeedNode(dbFactory, "main-1:4053", enabled: true, clusterId: "MAIN");

        EventFilter.Warning(contains: "main-1:4053").ExpectOne(TimeSpan.FromSeconds(10), () =>
            Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory, ownClusterId: "MAIN")));
    }

    /// <summary>
    ///     Legacy backward-compat: an admin node with no cluster role (null <c>ownClusterId</c>) still
    ///     reconciles the WHOLE fleet — such a node genuinely sees every member via gossip. A
    ///     <c>SITE-A</c> enabled row with no member is warned about, exactly as before the Phase 6 scope.
    /// </summary>
    [Fact]
    public void Foreign_cluster_row_is_still_logged_when_unscoped()
    {
        var dbFactory = NewInMemoryDbFactory();
        SeedNode(dbFactory, "site-a-1:4053", enabled: true, clusterId: "SITE-A");

        EventFilter.Warning(contains: "site-a-1:4053").ExpectOne(TimeSpan.FromSeconds(10), () =>
            Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory, ownClusterId: null)));
    }

    private static void SeedNode(
        IDbContextFactory<OtOpcUaConfigDbContext> dbFactory, string nodeId, bool enabled,
        string clusterId = "SITE-A")
    {
        using var db = dbFactory.CreateDbContext();
        if (!db.ServerClusters.Any(c => c.ClusterId == clusterId))
        {
            db.ServerClusters.Add(new ServerCluster
            {
                ClusterId = clusterId,
                Name = clusterId,
                Enterprise = "zb",
                Site = clusterId.ToLowerInvariant(),
                NodeCount = 2,
                RedundancyMode = RedundancyMode.Warm,
                CreatedBy = "test",
            });
        }

        db.ClusterNodes.Add(new ClusterNode
        {
            NodeId = nodeId,
            ClusterId = clusterId,
            Host = nodeId.Split(':')[0],
            ApplicationUri = $"urn:OtOpcUa:{nodeId}",
            Enabled = enabled,
            CreatedBy = "test",
        });
        db.SaveChanges();
    }
}
