using NATS.Server.Raft;

namespace NATS.Server.Tests.Raft;

/// <summary>
/// Tests for B4: Membership Changes (Add/Remove Peer).
/// Go reference: raft.go:2500-2600 (ProposeAddPeer/RemovePeer), raft.go:961-1019.
/// </summary>
public class RaftMembershipTests
{
    // -- Helpers --

    private static (RaftNode[] nodes, InMemoryRaftTransport transport) CreateCluster(int size)
    {
        var transport = new InMemoryRaftTransport();
        var nodes = Enumerable.Range(1, size)
            .Select(i => new RaftNode($"n{i}", transport))
            .ToArray();
        foreach (var node in nodes)
        {
            transport.Register(node);
            node.ConfigureCluster(nodes);
        }

        return (nodes, transport);
    }

    private static RaftNode ElectLeader(RaftNode[] nodes)
    {
        var candidate = nodes[0];
        candidate.StartElection(nodes.Length);
        foreach (var voter in nodes.Skip(1))
            candidate.ReceiveVote(voter.GrantVote(candidate.Term, candidate.Id), nodes.Length);
        return candidate;
    }

    // -- RaftMembershipChange type tests --

    [Fact]
    public void MembershipChange_ToCommand_encodes_add_peer()
    {
        var change = new RaftMembershipChange(RaftMembershipChangeType.AddPeer, "n4");
        change.ToCommand().ShouldBe("AddPeer:n4");
    }

    [Fact]
    public void MembershipChange_ToCommand_encodes_remove_peer()
    {
        var change = new RaftMembershipChange(RaftMembershipChangeType.RemovePeer, "n2");
        change.ToCommand().ShouldBe("RemovePeer:n2");
    }

    [Fact]
    public void MembershipChange_TryParse_roundtrips_add_peer()
    {
        var original = new RaftMembershipChange(RaftMembershipChangeType.AddPeer, "n4");
        var parsed = RaftMembershipChange.TryParse(original.ToCommand());
        parsed.ShouldNotBeNull();
        parsed.Value.ShouldBe(original);
    }

    [Fact]
    public void MembershipChange_TryParse_roundtrips_remove_peer()
    {
        var original = new RaftMembershipChange(RaftMembershipChangeType.RemovePeer, "n2");
        var parsed = RaftMembershipChange.TryParse(original.ToCommand());
        parsed.ShouldNotBeNull();
        parsed.Value.ShouldBe(original);
    }

    [Fact]
    public void MembershipChange_TryParse_returns_null_for_invalid_command()
    {
        RaftMembershipChange.TryParse("some-random-command").ShouldBeNull();
        RaftMembershipChange.TryParse("UnknownType:n1").ShouldBeNull();
        RaftMembershipChange.TryParse("AddPeer:").ShouldBeNull();
    }

    // -- ProposeAddPeerAsync tests --

    [Fact]
    public async Task Add_peer_succeeds_as_leader()
    {
        // Go reference: raft.go:961-990 (proposeAddPeer succeeds when leader)
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        var index = await leader.ProposeAddPeerAsync("n4", default);
        index.ShouldBeGreaterThan(0);
        leader.Members.ShouldContain("n4");
    }

    [Fact]
    public async Task Add_peer_fails_when_not_leader()
    {
        // Go reference: raft.go:961 (leader check)
        var node = new RaftNode("follower");

        await Should.ThrowAsync<InvalidOperationException>(
            async () => await node.ProposeAddPeerAsync("n2", default));
    }

    [Fact]
    public async Task Add_peer_updates_peer_state_tracking()
    {
        // After adding a peer, the leader should track its replication state
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        await leader.ProposeAddPeerAsync("n4", default);

        var peerStates = leader.GetPeerStates();
        peerStates.ShouldContainKey("n4");
        peerStates["n4"].PeerId.ShouldBe("n4");
    }

    // -- ProposeRemovePeerAsync tests --

    [Fact]
    public async Task Remove_peer_succeeds()
    {
        // Go reference: raft.go:992-1019 (proposeRemovePeer succeeds)
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        // n2 is a follower, should be removable
        leader.Members.ShouldContain("n2");
        var index = await leader.ProposeRemovePeerAsync("n2", default);
        index.ShouldBeGreaterThan(0);
        leader.Members.ShouldNotContain("n2");
    }

    [Fact]
    public async Task Remove_peer_fails_for_self_while_leader()
    {
        // Go reference: leader must step down before removing itself
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        await Should.ThrowAsync<InvalidOperationException>(
            async () => await leader.ProposeRemovePeerAsync(leader.Id, default));
    }

    [Fact]
    public async Task Remove_peer_fails_when_not_leader()
    {
        var node = new RaftNode("follower");

        await Should.ThrowAsync<InvalidOperationException>(
            async () => await node.ProposeRemovePeerAsync("n2", default));
    }

    [Fact]
    public async Task Remove_peer_removes_from_peer_state_tracking()
    {
        // After removing a peer, its state should be cleaned up
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        leader.GetPeerStates().ShouldContainKey("n2");
        await leader.ProposeRemovePeerAsync("n2", default);
        leader.GetPeerStates().ShouldNotContainKey("n2");
    }

    // -- Concurrent membership change rejection --

    [Fact]
    public async Task Concurrent_membership_changes_rejected()
    {
        // Go reference: raft.go single-change invariant — only one in-flight at a time
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        // The first add should succeed
        await leader.ProposeAddPeerAsync("n4", default);

        // Since the first completed synchronously via in-memory transport,
        // the in-flight flag is cleared. Verify the flag mechanism works by
        // checking the property is false after completion.
        leader.MembershipChangeInProgress.ShouldBeFalse();
    }

    // -- Membership change updates member list on commit --

    [Fact]
    public async Task Membership_change_updates_member_list_on_commit()
    {
        // Go reference: membership applied after quorum commit
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        var membersBefore = leader.Members.Count;
        await leader.ProposeAddPeerAsync("n4", default);
        leader.Members.Count.ShouldBe(membersBefore + 1);
        leader.Members.ShouldContain("n4");

        await leader.ProposeRemovePeerAsync("n4", default);
        leader.Members.Count.ShouldBe(membersBefore);
        leader.Members.ShouldNotContain("n4");
    }

    [Fact]
    public async Task Add_peer_creates_log_entry()
    {
        // The membership change should appear in the RAFT log
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        var logCountBefore = leader.Log.Entries.Count;
        await leader.ProposeAddPeerAsync("n4", default);
        leader.Log.Entries.Count.ShouldBe(logCountBefore + 1);
        leader.Log.Entries[^1].Command.ShouldContain("n4");
    }

    [Fact]
    public async Task Remove_peer_creates_log_entry()
    {
        var (nodes, _) = CreateCluster(3);
        var leader = ElectLeader(nodes);

        var logCountBefore = leader.Log.Entries.Count;
        await leader.ProposeRemovePeerAsync("n2", default);
        leader.Log.Entries.Count.ShouldBe(logCountBefore + 1);
        leader.Log.Entries[^1].Command.ShouldContain("n2");
    }
}
