using NATS.Server;
using NATS.Server.Auth;
using NATS.Server.Raft;

namespace NATS.Server.Tests.Raft;

/// <summary>
/// Tests for NatsRaftTransport — verifies subject routing, wire encoding,
/// and that the transport can be constructed with an InternalClient.
///
/// Go reference: golang/nats-server/server/raft.go:2192-2230 (subject setup),
///               2854-2970 (send helpers), 2161-2169 (subject constants).
/// </summary>
public class NatsRaftTransportTests
{
    // ---------------------------------------------------------------------------
    // Construction
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2210 — n.vsubj, n.vreply = fmt.Sprintf(raftVoteSubj, n.group)...
    [Fact]
    public void Transport_can_be_constructed_with_internal_client()
    {
        var account = new Account("$G");
        var client = new InternalClient(1UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (subject, reply, payload) => { });

        transport.ShouldNotBeNull();
        transport.GroupId.ShouldBe("meta");
        transport.Client.ShouldBeSameAs(client);
    }

    [Fact]
    public void Transport_exposes_group_id()
    {
        var account = new Account("$G");
        var client = new InternalClient(2UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "stream-A",
            (_, _, _) => { });

        transport.GroupId.ShouldBe("stream-A");
    }

    [Fact]
    public void Transport_throws_when_client_is_null()
    {
        Should.Throw<ArgumentNullException>(
            () => new NatsRaftTransport(null!, "meta", (_, _, _) => { }));
    }

    [Fact]
    public void Transport_throws_when_groupId_is_empty()
    {
        var account = new Account("$G");
        var client = new InternalClient(3UL, ClientKind.System, account);

        Should.Throw<ArgumentException>(
            () => new NatsRaftTransport(client, "", (_, _, _) => { }));
    }

    [Fact]
    public void Transport_throws_when_publish_is_null()
    {
        var account = new Account("$G");
        var client = new InternalClient(4UL, ClientKind.System, account);

        Should.Throw<ArgumentNullException>(
            () => new NatsRaftTransport(client, "meta", null!));
    }

    // ---------------------------------------------------------------------------
    // AppendEntries — subject routing
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2164 — n.asubj = fmt.Sprintf(raftAppendSubj, n.group)
    [Fact]
    public async Task AppendEntries_publishes_to_NRG_AE_subject()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(10UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (subject, _, _) => capturedSubject = subject);

        var entry = new RaftLogEntry(Index: 1, Term: 1, Command: "op");
        await transport.AppendEntriesAsync("leader1", ["peer1"], entry, CancellationToken.None);

        capturedSubject.ShouldBe("$NRG.AE.meta");
    }

    // Go: server/raft.go:2164 — subject varies by group name
    [Fact]
    public async Task AppendEntries_subject_includes_group_name()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(11UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "stream-orders",
            (subject, _, _) => capturedSubject = subject);

        var entry = new RaftLogEntry(Index: 1, Term: 1, Command: "op");
        await transport.AppendEntriesAsync("leader1", ["peer1"], entry, CancellationToken.None);

        capturedSubject.ShouldBe("$NRG.AE.stream-orders");
    }

    // Go: server/raft.go:2167 — reply inbox set to raftReply format
    [Fact]
    public async Task AppendEntries_includes_NRG_R_reply_subject()
    {
        var capturedReply = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(12UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, reply, _) => capturedReply = reply ?? string.Empty);

        var entry = new RaftLogEntry(Index: 1, Term: 1, Command: "op");
        await transport.AppendEntriesAsync("leader1", ["peer1"], entry, CancellationToken.None);

        capturedReply.ShouldStartWith("$NRG.R.");
    }

    // ---------------------------------------------------------------------------
    // AppendEntries — wire encoding
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2662-2711 — appendEntry.encode()
    [Fact]
    public async Task AppendEntries_encodes_leader_id_in_wire_payload()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(13UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var entry = new RaftLogEntry(Index: 3, Term: 2, Command: "x");
        await transport.AppendEntriesAsync("leader1", ["peer1"], entry, CancellationToken.None);

        capturedPayload.IsEmpty.ShouldBeFalse();
        var decoded = RaftAppendEntryWire.Decode(capturedPayload.Span);
        decoded.LeaderId.ShouldBe("leader1");
    }

    // Go: server/raft.go:2694 — ae.term written to wire
    [Fact]
    public async Task AppendEntries_encodes_term_in_wire_payload()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(14UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var entry = new RaftLogEntry(Index: 5, Term: 7, Command: "cmd");
        await transport.AppendEntriesAsync("L", ["peer1"], entry, CancellationToken.None);

        var decoded = RaftAppendEntryWire.Decode(capturedPayload.Span);
        decoded.Term.ShouldBe(7UL);
    }

    // Go: server/raft.go:2699-2705 — entry data encoded in payload
    [Fact]
    public async Task AppendEntries_encodes_command_as_normal_entry()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(15UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var entry = new RaftLogEntry(Index: 1, Term: 1, Command: "hello");
        await transport.AppendEntriesAsync("L", ["peer1"], entry, CancellationToken.None);

        var decoded = RaftAppendEntryWire.Decode(capturedPayload.Span);
        decoded.Entries.Count.ShouldBe(1);
        decoded.Entries[0].Type.ShouldBe(RaftEntryType.Normal);
        System.Text.Encoding.UTF8.GetString(decoded.Entries[0].Data).ShouldBe("hello");
    }

    // AppendEntries returns one result per follower
    [Fact]
    public async Task AppendEntries_returns_result_per_follower()
    {
        var account = new Account("$G");
        var client = new InternalClient(16UL, ClientKind.System, account);
        var transport = new NatsRaftTransport(client, "meta", (_, _, _) => { });

        var entry = new RaftLogEntry(Index: 1, Term: 1, Command: "op");
        var results = await transport.AppendEntriesAsync("L", ["peer1", "peer2", "peer3"],
            entry, CancellationToken.None);

        results.Count.ShouldBe(3);
        results.Select(r => r.FollowerId).ShouldBe(["peer1", "peer2", "peer3"], ignoreOrder: false);
    }

    // ---------------------------------------------------------------------------
    // RequestVote — subject routing
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2163 — n.vsubj = fmt.Sprintf(raftVoteSubj, n.group)
    [Fact]
    public async Task RequestVote_publishes_to_NRG_V_subject()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(20UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (subject, _, _) => capturedSubject = subject);

        var req = new VoteRequest { Term = 3, CandidateId = "cand1" };
        await transport.RequestVoteAsync("cand1", "voter1", req, CancellationToken.None);

        capturedSubject.ShouldBe("$NRG.V.meta");
    }

    // Go: server/raft.go:2163 — subject varies by group name
    [Fact]
    public async Task RequestVote_subject_includes_group_name()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(21UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "stream-events",
            (subject, _, _) => capturedSubject = subject);

        var req = new VoteRequest { Term = 1, CandidateId = "c" };
        await transport.RequestVoteAsync("c", "v", req, CancellationToken.None);

        capturedSubject.ShouldBe("$NRG.V.stream-events");
    }

    // Go: server/raft.go:2167 — n.vreply = n.newInbox() → "$NRG.R.{suffix}"
    [Fact]
    public async Task RequestVote_includes_NRG_R_reply_subject()
    {
        var capturedReply = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(22UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, reply, _) => capturedReply = reply ?? string.Empty);

        var req = new VoteRequest { Term = 1, CandidateId = "c" };
        await transport.RequestVoteAsync("c", "v", req, CancellationToken.None);

        capturedReply.ShouldStartWith("$NRG.R.");
    }

    // ---------------------------------------------------------------------------
    // RequestVote — wire encoding
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:4560-4568 — voteRequest.encode()
    [Fact]
    public async Task RequestVote_encodes_term_in_wire_payload()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(23UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var req = new VoteRequest { Term = 9, CandidateId = "cand1" };
        await transport.RequestVoteAsync("cand1", "voter1", req, CancellationToken.None);

        capturedPayload.Length.ShouldBe(RaftWireConstants.VoteRequestLen); // 32 bytes
        var decoded = RaftVoteRequestWire.Decode(capturedPayload.Span);
        decoded.Term.ShouldBe(9UL);
    }

    // Go: server/raft.go:4567 — candidateId written to wire
    [Fact]
    public async Task RequestVote_uses_candidate_id_from_request_when_set()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(24UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var req = new VoteRequest { Term = 2, CandidateId = "cand99" };
        await transport.RequestVoteAsync("fallback", "voter1", req, CancellationToken.None);

        var decoded = RaftVoteRequestWire.Decode(capturedPayload.Span);
        // CandidateId from request takes precedence, truncated to 8 chars (idLen)
        decoded.CandidateId.ShouldBe("cand99");
    }

    // Go: server/raft.go:4567 — candidateId falls back to candidateId param when request id is empty
    [Fact]
    public async Task RequestVote_uses_caller_candidate_id_when_request_id_empty()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(25UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var req = new VoteRequest { Term = 1, CandidateId = "" };
        await transport.RequestVoteAsync("fallbk", "voter1", req, CancellationToken.None);

        var decoded = RaftVoteRequestWire.Decode(capturedPayload.Span);
        decoded.CandidateId.ShouldBe("fallbk");
    }

    // ---------------------------------------------------------------------------
    // InstallSnapshot — subject routing
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2168 — raftCatchupReply = "$NRG.CR.%s"
    [Fact]
    public async Task InstallSnapshot_publishes_to_NRG_CR_subject()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(30UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (subject, _, _) => capturedSubject = subject);

        var snapshot = new RaftSnapshot { LastIncludedIndex = 10, LastIncludedTerm = 2, Data = [1, 2, 3] };
        await transport.InstallSnapshotAsync("leader1", "peer1", snapshot, CancellationToken.None);

        capturedSubject.ShouldStartWith("$NRG.CR.");
    }

    // Go: server/raft.go:2168 — no reply-to for catchup transfers
    [Fact]
    public async Task InstallSnapshot_has_no_reply_subject()
    {
        string? capturedReply = "not-null";
        var account = new Account("$G");
        var client = new InternalClient(31UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, reply, _) => capturedReply = reply);

        var snapshot = new RaftSnapshot { LastIncludedIndex = 5, LastIncludedTerm = 1, Data = [] };
        await transport.InstallSnapshotAsync("L", "P", snapshot, CancellationToken.None);

        capturedReply.ShouldBeNull();
    }

    // ---------------------------------------------------------------------------
    // InstallSnapshot — wire encoding
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:3247 — snapshot encoded as EntryOldSnapshot AppendEntry
    [Fact]
    public async Task InstallSnapshot_encodes_data_as_old_snapshot_entry()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(32UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var snapshotData = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
        var snapshot = new RaftSnapshot { LastIncludedIndex = 100, LastIncludedTerm = 5, Data = snapshotData };
        await transport.InstallSnapshotAsync("L", "P", snapshot, CancellationToken.None);

        capturedPayload.IsEmpty.ShouldBeFalse();
        var decoded = RaftAppendEntryWire.Decode(capturedPayload.Span);
        decoded.Entries.Count.ShouldBe(1);
        decoded.Entries[0].Type.ShouldBe(RaftEntryType.OldSnapshot);
        decoded.Entries[0].Data.ShouldBe(snapshotData);
    }

    // ---------------------------------------------------------------------------
    // ForwardProposal — subject routing
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2165 — n.psubj = fmt.Sprintf(raftPropSubj, n.group)
    [Fact]
    public void ForwardProposal_publishes_to_NRG_P_subject()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(40UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (subject, _, _) => capturedSubject = subject);

        transport.ForwardProposal(new byte[] { 1, 2, 3 });

        capturedSubject.ShouldBe("$NRG.P.meta");
    }

    // Go: server/raft.go:2165 — subject varies by group name
    [Fact]
    public void ForwardProposal_subject_includes_group_name()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(41UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "stream-inventory",
            (subject, _, _) => capturedSubject = subject);

        transport.ForwardProposal(System.Text.Encoding.UTF8.GetBytes("entry"));

        capturedSubject.ShouldBe("$NRG.P.stream-inventory");
    }

    // Go: server/raft.go:949 — payload forwarded verbatim
    [Fact]
    public void ForwardProposal_sends_payload_verbatim()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(42UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        var data = new byte[] { 10, 20, 30, 40 };
        transport.ForwardProposal(data);

        capturedPayload.ToArray().ShouldBe(data);
    }

    // ---------------------------------------------------------------------------
    // ProposeRemovePeer — subject routing
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2166 — n.rpsubj = fmt.Sprintf(raftRemovePeerSubj, n.group)
    [Fact]
    public void ProposeRemovePeer_publishes_to_NRG_RP_subject()
    {
        var capturedSubject = string.Empty;
        var account = new Account("$G");
        var client = new InternalClient(50UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (subject, _, _) => capturedSubject = subject);

        transport.ProposeRemovePeer("peer-x");

        capturedSubject.ShouldBe("$NRG.RP.meta");
    }

    // Go: server/raft.go:986 — peer name encoded as UTF-8 bytes
    [Fact]
    public void ProposeRemovePeer_encodes_peer_name_as_utf8()
    {
        ReadOnlyMemory<byte> capturedPayload = default;
        var account = new Account("$G");
        var client = new InternalClient(51UL, ClientKind.System, account);

        var transport = new NatsRaftTransport(client, "meta",
            (_, _, payload) => capturedPayload = payload);

        transport.ProposeRemovePeer("peer-abc");

        System.Text.Encoding.UTF8.GetString(capturedPayload.Span).ShouldBe("peer-abc");
    }

    // ---------------------------------------------------------------------------
    // IRaftTransport implementation
    // ---------------------------------------------------------------------------

    // NatsRaftTransport must implement IRaftTransport
    [Fact]
    public void NatsRaftTransport_implements_IRaftTransport()
    {
        var account = new Account("$G");
        var client = new InternalClient(60UL, ClientKind.System, account);
        var transport = new NatsRaftTransport(client, "meta", (_, _, _) => { });

        (transport as IRaftTransport).ShouldNotBeNull();
    }
}
