using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Redundancy;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;

/// <summary>
///     archreview 03/S4 — the single Primary data-plane gate decision. A KNOWN role wins outright; an
///     UNKNOWN role (no snapshot yet, or the snapshot never mentioned this node) is resolved by cluster
///     membership: a single-driver cluster stays default-ALLOW (boot-window / single-node posture), a
///     multi-driver cluster is default-DENY (a real Primary peer exists — don't touch the shared field
///     device until the snapshot proves this node is Primary).
/// </summary>
public sealed class PrimaryGatePolicyTests
{
    [Theory]
    // Primary → always service, regardless of member count.
    [InlineData(RedundancyRole.Primary, 0, true)]
    [InlineData(RedundancyRole.Primary, 1, true)]
    [InlineData(RedundancyRole.Primary, 2, true)]
    // Secondary / Detached → never service, regardless of member count.
    [InlineData(RedundancyRole.Secondary, 0, false)]
    [InlineData(RedundancyRole.Secondary, 2, false)]
    [InlineData(RedundancyRole.Detached, 0, false)]
    [InlineData(RedundancyRole.Detached, 2, false)]
    public void Known_role_wins_regardless_of_member_count(RedundancyRole role, int members, bool expected)
        => PrimaryGatePolicy.ShouldServiceAsPrimary(role, members).ShouldBe(expected);

    [Theory]
    // Unknown role: allow only when no driver peer exists (count <= 1).
    [InlineData(0, true)]
    [InlineData(1, true)]
    [InlineData(2, false)]
    [InlineData(3, false)]
    public void Unknown_role_resolves_by_membership(int members, bool expected)
        => PrimaryGatePolicy.ShouldServiceAsPrimary(null, members).ShouldBe(expected);
}

/// <summary>
///     The alarm-history drain gate, which deliberately does <b>not</b> mirror the device-write gate.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why a second policy is correct here.</b> The write gate protects a shared field device:
///         two nodes driving one PLC is dangerous and irreversible, so an unknown role with a peer
///         present must deny. The drain writes to an append-only historian instead, and Phase 2's
///         delivery contract is already <i>at-least-once across a failover, by design</i> — identical
///         events even collapse to one row, because ids are a hash of the payload.
///     </para>
///     <para>
///         So the two error costs are wildly asymmetric: a false allow costs a duplicate history row,
///         while a false deny silently stops the alarm audit trail and eventually evicts it at the
///         capacity wall. This gate therefore keys on the role <b>alone</b> and opens when the role is
///         unknown.
///     </para>
///     <para>
///         <b>Found by the LocalDb Phase 2 live gate.</b> The rig runs four driver nodes in one Akka
///         cluster with no <c>Redundancy</c> section, so every node's role was unknown and the
///         cluster-wide driver count was 4 — and every node, including the two unpaired site-b nodes
///         that have no peer and no replication at all, logged "Historian drain suspended". Nothing
///         drained anywhere. Before Phase 2 that deployment drained fine, because the drain was ungated.
///     </para>
/// </remarks>
public sealed class AlarmHistoryDrainGatePolicyTests
{
    [Theory]
    // A Primary always drains its own queue.
    [InlineData(RedundancyRole.Primary, true, true)]
    [InlineData(RedundancyRole.Primary, false, true)]
    // A Secondary stands down ONLY when a Primary actually exists to stand up.
    [InlineData(RedundancyRole.Secondary, true, false)]
    [InlineData(RedundancyRole.Detached, true, false)]
    [InlineData(RedundancyRole.Secondary, false, true)]
    [InlineData(RedundancyRole.Detached, false, true)]
    public void A_known_role_stands_down_only_for_a_primary_that_exists(
        RedundancyRole role, bool queueSharingPeerIsPrimary, bool expected)
        => PrimaryGatePolicy.ShouldDrainAlarmHistory(role, queueSharingPeerIsPrimary).ShouldBe(expected);

    /// <summary>
    ///     Unknown role ⇒ drain, whoever else is out there. The first case the live gate broke on, and
    ///     the one that separates this policy from <see cref="PrimaryGatePolicy.ShouldServiceAsPrimary"/>.
    /// </summary>
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void An_unknown_role_drains(bool queueSharingPeerIsPrimary)
        => PrimaryGatePolicy.ShouldDrainAlarmHistory(null, queueSharingPeerIsPrimary).ShouldBeTrue();

    /// <summary>
    ///     A Secondary whose Primary is NOT its queue-sharing peer keeps draining — the deepest of the
    ///     live gate's findings.
    /// </summary>
    /// <remarks>
    ///     <c>RedundancyStateActor</c> derives roles from Akka's cluster-wide <c>RoleLeader("driver")</c>,
    ///     but the alarm queue is pair-local. On the docker-dev rig the elected Primary is a CENTRAL node
    ///     — it carries the driver Akka role, replicates nobody's LocalDb, and does not even run the
    ///     alarm historian — so under a plain "a Primary exists ⇒ stand down" rule every site node
    ///     suspended its drain in favour of a node that could not possibly deliver its events. The
    ///     buffer then grows on every node until the capacity wall evicts the oldest: silent, permanent
    ///     loss of the audit trail the queue exists to protect.
    /// </remarks>
    [Fact]
    public void A_secondary_whose_peer_is_not_the_primary_keeps_draining()
    {
        // A Primary exists somewhere in the cluster, but it does not hold this node's rows.
        PrimaryGatePolicy.ShouldDrainAlarmHistory(RedundancyRole.Secondary, queueSharingPeerIsPrimary: false)
            .ShouldBeTrue("a Secondary must not defer to a Primary that cannot deliver its events");
    }

    /// <summary>
    ///     The divergence, pinned explicitly: with a peer present and no role known, the write gate
    ///     denies and the drain gate allows. If someone later "harmonises" the two, this fails.
    /// </summary>
    [Fact]
    public void The_two_gates_deliberately_disagree_when_the_role_is_unknown_and_a_peer_exists()
    {
        PrimaryGatePolicy.ShouldServiceAsPrimary(localRole: null, driverMemberCount: 4).ShouldBeFalse();
        PrimaryGatePolicy.ShouldDrainAlarmHistory(localRole: null, queueSharingPeerIsPrimary: true).ShouldBeTrue();
    }
}
