// Go parity: golang/nats-server/server/jetstream_cluster.go
// Covers: RaftGroup member management, peer add/remove/preferred,
// factory method via PlacementEngine, replication health properties,
// and quorum size calculation.
using NATS.Server.JetStream.Cluster;

namespace NATS.Server.JetStream.Tests.JetStream.Cluster;

/// <summary>
/// Tests for RaftGroup lifecycle: membership helpers, factory method,
/// replication status properties, and quorum size.
/// Go reference: jetstream_cluster.go:154-163 raftGroup struct and peer management.
/// </summary>
public class RaftGroupLifecycleTests
{
    // ---------------------------------------------------------------
    // IsMember — membership check
    // Go reference: jetstream_cluster.go isMember helper
    // ---------------------------------------------------------------

    [Fact]
    public void IsMember_returns_true_for_existing_peer()
    {
        // Go reference: jetstream_cluster.go isMember — checks rg.Peers contains id
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"] };

        group.IsMember("peer-2").ShouldBeTrue();
    }

    [Fact]
    public void IsMember_returns_false_for_non_member()
    {
        // Go reference: jetstream_cluster.go isMember — returns false when not in Peers
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] };

        group.IsMember("peer-9").ShouldBeFalse();
    }

    // ---------------------------------------------------------------
    // SetPreferred — assign preferred peer
    // Go reference: jetstream_cluster.go setPreferred / rg.Preferred
    // ---------------------------------------------------------------

    [Fact]
    public void SetPreferred_sets_preferred_peer()
    {
        // Go reference: jetstream_cluster.go setPreferred — assigns rg.Preferred when member
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"] };

        group.SetPreferred("peer-3");

        group.Preferred.ShouldBe("peer-3");
    }

    [Fact]
    public void SetPreferred_throws_for_non_member()
    {
        // Go reference: jetstream_cluster.go setPreferred — validates membership before setting
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] };

        Should.Throw<InvalidOperationException>(() => group.SetPreferred("peer-99"));
    }

    // ---------------------------------------------------------------
    // RemovePeer — remove a peer from the group
    // Go reference: jetstream_cluster.go removePeer
    // ---------------------------------------------------------------

    [Fact]
    public void RemovePeer_removes_existing_peer()
    {
        // Go reference: jetstream_cluster.go removePeer — removes peer from rg.Peers
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"] };

        var removed = group.RemovePeer("peer-2");

        removed.ShouldBeTrue();
        group.Peers.ShouldNotContain("peer-2");
        group.Peers.Count.ShouldBe(2);
    }

    [Fact]
    public void RemovePeer_clears_preferred_when_removing_preferred()
    {
        // Go reference: jetstream_cluster.go removePeer — clears rg.Preferred if it matches removed peer
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"], Preferred = "peer-2" };

        group.RemovePeer("peer-2");

        group.Preferred.ShouldBe(string.Empty);
    }

    [Fact]
    public void RemovePeer_returns_false_for_non_member()
    {
        // Go reference: jetstream_cluster.go removePeer — returns false when peer not found
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] };

        var removed = group.RemovePeer("peer-99");

        removed.ShouldBeFalse();
        group.Peers.Count.ShouldBe(2);
    }

    // ---------------------------------------------------------------
    // AddPeer — add a new peer to the group
    // Go reference: jetstream_cluster.go addPeer / expandGroup
    // ---------------------------------------------------------------

    [Fact]
    public void AddPeer_adds_new_peer()
    {
        // Go reference: jetstream_cluster.go addPeer — appends peer to rg.Peers
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] };

        var added = group.AddPeer("peer-3");

        added.ShouldBeTrue();
        group.Peers.ShouldContain("peer-3");
        group.Peers.Count.ShouldBe(3);
    }

    [Fact]
    public void AddPeer_returns_false_for_existing_peer()
    {
        // Go reference: jetstream_cluster.go addPeer — skips duplicate, returns false
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] };

        var added = group.AddPeer("peer-1");

        added.ShouldBeFalse();
        group.Peers.Count.ShouldBe(2);
    }

    // ---------------------------------------------------------------
    // CreateRaftGroup factory — uses PlacementEngine
    // Go reference: jetstream_cluster.go:7212 selectPeerGroup called from createGroupForStream
    // ---------------------------------------------------------------

    [Fact]
    public void CreateRaftGroup_uses_placement_engine()
    {
        // Go reference: jetstream_cluster.go createGroupForStream — calls selectPeerGroup
        var peers = new List<PeerInfo>
        {
            new() { PeerId = "peer-A", Available = true, AvailableStorage = 9000 },
            new() { PeerId = "peer-B", Available = true, AvailableStorage = 8000 },
            new() { PeerId = "peer-C", Available = true, AvailableStorage = 7000 },
        };

        var group = RaftGroup.CreateRaftGroup("my-stream", 3, peers);

        group.Name.ShouldBe("my-stream");
        group.Peers.Count.ShouldBe(3);
        group.Peers.ShouldContain("peer-A");
        group.Peers.ShouldContain("peer-B");
        group.Peers.ShouldContain("peer-C");
    }

    [Fact]
    public void CreateRaftGroup_sets_desired_replicas()
    {
        // Go reference: jetstream_cluster.go rg.DesiredReplicas = replicas after group creation
        var peers = new List<PeerInfo>
        {
            new() { PeerId = "peer-X", Available = true },
            new() { PeerId = "peer-Y", Available = true },
            new() { PeerId = "peer-Z", Available = true },
        };

        var group = RaftGroup.CreateRaftGroup("replicated-stream", 3, peers);

        group.DesiredReplicas.ShouldBe(3);
        group.HasDesiredReplicas.ShouldBeTrue();
    }

    // ---------------------------------------------------------------
    // IsUnderReplicated — replication health
    // Go reference: jetstream_cluster.go missingPeers — len(Peers) < DesiredReplicas
    // ---------------------------------------------------------------

    [Fact]
    public void IsUnderReplicated_true_when_peers_less_than_desired()
    {
        // Go reference: jetstream_cluster.go:2284 sa.missingPeers()
        var group = new RaftGroup { Name = "test", Peers = ["peer-1"], DesiredReplicas = 3 };

        group.IsUnderReplicated.ShouldBeTrue();
    }

    [Fact]
    public void IsUnderReplicated_false_when_peers_equal_desired()
    {
        // Go reference: jetstream_cluster.go:2284 sa.missingPeers() — no deficit when equal
        var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"], DesiredReplicas = 3 };

        group.IsUnderReplicated.ShouldBeFalse();
    }

    [Fact]
    public void IsUnderReplicated_false_when_no_desired_replicas_set()
    {
        // Go reference: jetstream_cluster.go — without DesiredReplicas set, no under-replication
        var group = new RaftGroup { Name = "test", Peers = ["peer-1"] };

        group.IsUnderReplicated.ShouldBeFalse();
    }

    // ---------------------------------------------------------------
    // IsOverReplicated — excess replication detection
    // Go reference: jetstream_cluster.go extraPeers — len(Peers) > DesiredReplicas
    // ---------------------------------------------------------------

    [Fact]
    public void IsOverReplicated_true_when_peers_more_than_desired()
    {
        // Go reference: jetstream_cluster.go extraPeers detection for scale-down
        var group = new RaftGroup { Name = "test", Peers = ["p1", "p2", "p3", "p4"], DesiredReplicas = 3 };

        group.IsOverReplicated.ShouldBeTrue();
    }

    [Fact]
    public void IsOverReplicated_false_when_peers_equal_desired()
    {
        // Go reference: jetstream_cluster.go — no excess when equal
        var group = new RaftGroup { Name = "test", Peers = ["p1", "p2", "p3"], DesiredReplicas = 3 };

        group.IsOverReplicated.ShouldBeFalse();
    }

    [Fact]
    public void IsOverReplicated_false_when_no_desired_replicas_set()
    {
        // Go reference: jetstream_cluster.go — without DesiredReplicas set, no over-replication
        var group = new RaftGroup { Name = "test", Peers = ["p1", "p2", "p3", "p4"] };

        group.IsOverReplicated.ShouldBeFalse();
    }

    // ---------------------------------------------------------------
    // QuorumSize — majority quorum calculation
    // Go reference: jetstream_cluster.go quorumNeeded — (n/2)+1
    // ---------------------------------------------------------------

    [Theory]
    [InlineData(1, 1)]   // R=1 → quorum=1
    [InlineData(3, 2)]   // R=3 → quorum=2
    [InlineData(5, 3)]   // R=5 → quorum=3
    [InlineData(2, 2)]   // R=2 → quorum=2 (degenerate, but formula consistent)
    [InlineData(4, 3)]   // R=4 → quorum=3
    public void QuorumSize_correct_for_various_counts(int peerCount, int expectedQuorum)
    {
        // Go reference: jetstream_cluster.go quorumNeeded — (n/2)+1
        var peers = Enumerable.Range(1, peerCount).Select(i => $"peer-{i}").ToList();
        var group = new RaftGroup { Name = "quorum-test", Peers = peers };

        group.QuorumSize.ShouldBe(expectedQuorum);
    }
}
