using NATS.Server.Raft;

namespace NATS.Server.Tests.Raft;

/// <summary>
/// Binary wire format encoding/decoding tests for all RAFT RPC types.
/// These validate exact byte-for-byte fidelity with Go's raft.go encoding.
/// Go reference: golang/nats-server/server/raft.go lines 2662-2796 (AppendEntry),
///               4560-4768 (vote types).
/// </summary>
public class RaftBinaryWireFormatTests
{
    // ---------------------------------------------------------------------------
    // VoteRequest
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:4560-4568 — voteRequest.encode()
    // Go: server/raft.go:4571-4583 — decodeVoteRequest()
    [Fact]
    public void VoteRequest_round_trip_encode_decode()
    {
        var original = new RaftVoteRequestWire(
            Term: 7,
            LastTerm: 3,
            LastIndex: 42,
            CandidateId: "peer0001");

        var encoded = original.Encode();
        encoded.Length.ShouldBe(RaftWireConstants.VoteRequestLen); // 32 bytes

        var decoded = RaftVoteRequestWire.Decode(encoded);
        decoded.Term.ShouldBe(7UL);
        decoded.LastTerm.ShouldBe(3UL);
        decoded.LastIndex.ShouldBe(42UL);
        decoded.CandidateId.ShouldBe("peer0001");
    }

    [Fact]
    public void VoteRequest_bytes_are_little_endian()
    {
        var req = new RaftVoteRequestWire(Term: 1, LastTerm: 0, LastIndex: 0, CandidateId: "");
        var bytes = req.Encode();
        // term = 1 in little-endian: [1, 0, 0, 0, 0, 0, 0, 0]
        // Go: server/raft.go:4563 — le.PutUint64(buf[0:], vr.term)
        bytes[0].ShouldBe((byte)1);
        bytes[1].ShouldBe((byte)0);
    }

    [Fact]
    public void VoteRequest_zero_values_encode_to_zeroed_buffer()
    {
        var req = new RaftVoteRequestWire(Term: 0, LastTerm: 0, LastIndex: 0, CandidateId: "");
        var bytes = req.Encode();
        bytes.Length.ShouldBe(32);
        bytes.ShouldAllBe(b => b == 0);
    }

    [Fact]
    public void VoteRequest_large_term_round_trips()
    {
        var req = new RaftVoteRequestWire(
            Term: ulong.MaxValue,
            LastTerm: ulong.MaxValue - 1,
            LastIndex: ulong.MaxValue - 2,
            CandidateId: "node1234");

        var decoded = RaftVoteRequestWire.Decode(req.Encode());
        decoded.Term.ShouldBe(ulong.MaxValue);
        decoded.LastTerm.ShouldBe(ulong.MaxValue - 1);
        decoded.LastIndex.ShouldBe(ulong.MaxValue - 2);
        decoded.CandidateId.ShouldBe("node1234");
    }

    [Fact]
    public void VoteRequest_short_buffer_throws_ArgumentException()
    {
        var shortBuffer = new byte[RaftWireConstants.VoteRequestLen - 1];
        Should.Throw<ArgumentException>(() => RaftVoteRequestWire.Decode(shortBuffer));
    }

    [Fact]
    public void VoteRequest_long_buffer_throws_ArgumentException()
    {
        var longBuffer = new byte[RaftWireConstants.VoteRequestLen + 1];
        Should.Throw<ArgumentException>(() => RaftVoteRequestWire.Decode(longBuffer));
    }

    [Fact]
    public void VoteRequest_candidate_id_truncated_to_8_bytes()
    {
        // IDs longer than 8 chars are silently truncated (Go copy semantics).
        // Go: server/raft.go:4566 — copy(buf[24:24+idLen], vr.candidate)
        var req = new RaftVoteRequestWire(
            Term: 1, LastTerm: 0, LastIndex: 0,
            CandidateId: "abcdefghXXXXXXXX"); // 16 chars; only first 8 kept

        var bytes = req.Encode();
        // Check that the ID field contains only the first 8 chars.
        var idBytes = bytes[24..32];
        System.Text.Encoding.ASCII.GetString(idBytes).ShouldBe("abcdefgh");
    }

    [Fact]
    public void VoteRequest_short_candidate_id_zero_padded()
    {
        var req = new RaftVoteRequestWire(
            Term: 1, LastTerm: 0, LastIndex: 0, CandidateId: "abc");

        var bytes = req.Encode();
        bytes[27].ShouldBe((byte)0); // byte 3..7 should be zero
        bytes[28].ShouldBe((byte)0);

        // Decode should recover the original 3-char ID.
        var decoded = RaftVoteRequestWire.Decode(bytes);
        decoded.CandidateId.ShouldBe("abc");
    }

    // ---------------------------------------------------------------------------
    // VoteResponse
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:4739-4751 — voteResponse.encode()
    // Go: server/raft.go:4753-4762 — decodeVoteResponse()
    [Fact]
    public void VoteResponse_granted_true_round_trip()
    {
        var resp = new RaftVoteResponseWire(Term: 5, PeerId: "peer0002", Granted: true);
        var decoded = RaftVoteResponseWire.Decode(resp.Encode());

        decoded.Term.ShouldBe(5UL);
        decoded.PeerId.ShouldBe("peer0002");
        decoded.Granted.ShouldBeTrue();
        decoded.Empty.ShouldBeFalse();
    }

    [Fact]
    public void VoteResponse_granted_false_round_trip()
    {
        var resp = new RaftVoteResponseWire(Term: 3, PeerId: "peer0003", Granted: false);
        var decoded = RaftVoteResponseWire.Decode(resp.Encode());

        decoded.Granted.ShouldBeFalse();
        decoded.PeerId.ShouldBe("peer0003");
    }

    [Fact]
    public void VoteResponse_empty_flag_round_trip()
    {
        // Go: server/raft.go:4746-4748 — buf[16] |= 2 when empty
        var resp = new RaftVoteResponseWire(Term: 1, PeerId: "p1", Granted: false, Empty: true);
        var decoded = RaftVoteResponseWire.Decode(resp.Encode());

        decoded.Empty.ShouldBeTrue();
        decoded.Granted.ShouldBeFalse();
    }

    [Fact]
    public void VoteResponse_both_flags_set()
    {
        var resp = new RaftVoteResponseWire(Term: 1, PeerId: "p1", Granted: true, Empty: true);
        var bytes = resp.Encode();

        // Go: server/raft.go:4744-4748 — bit 0 = granted, bit 1 = empty
        (bytes[16] & 1).ShouldBe(1); // granted
        (bytes[16] & 2).ShouldBe(2); // empty

        var decoded = RaftVoteResponseWire.Decode(bytes);
        decoded.Granted.ShouldBeTrue();
        decoded.Empty.ShouldBeTrue();
    }

    [Fact]
    public void VoteResponse_fixed_17_bytes()
    {
        var resp = new RaftVoteResponseWire(Term: 10, PeerId: "peer0001", Granted: true);
        resp.Encode().Length.ShouldBe(RaftWireConstants.VoteResponseLen); // 17
    }

    [Fact]
    public void VoteResponse_short_buffer_throws_ArgumentException()
    {
        var shortBuffer = new byte[RaftWireConstants.VoteResponseLen - 1];
        Should.Throw<ArgumentException>(() => RaftVoteResponseWire.Decode(shortBuffer));
    }

    [Fact]
    public void VoteResponse_peer_id_truncated_to_8_bytes()
    {
        // Go: server/raft.go:4743 — copy(buf[8:], vr.peer)
        var resp = new RaftVoteResponseWire(
            Term: 1, PeerId: "longpeernamethatexceeds8chars", Granted: true);
        var bytes = resp.Encode();

        // Bytes [8..15] hold the peer ID — only first 8 chars fit.
        var idBytes = bytes[8..16];
        System.Text.Encoding.ASCII.GetString(idBytes).ShouldBe("longpeer");
    }

    // ---------------------------------------------------------------------------
    // AppendEntry — zero entries
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2662-2711 — appendEntry.encode()
    // Go: server/raft.go:2714-2746 — decodeAppendEntry()
    [Fact]
    public void AppendEntry_zero_entries_round_trip()
    {
        var ae = new RaftAppendEntryWire(
            LeaderId: "lead0001",
            Term: 10,
            Commit: 8,
            PrevTerm: 9,
            PrevIndex: 7,
            Entries: [],
            LeaderTerm: 0);

        var encoded = ae.Encode();
        // Base length + 1-byte uvarint(0) for lterm.
        // Go: server/raft.go:2681-2683 — lterm uvarint always appended
        encoded.Length.ShouldBe(RaftWireConstants.AppendEntryBaseLen + 1);

        var decoded = RaftAppendEntryWire.Decode(encoded);
        decoded.LeaderId.ShouldBe("lead0001");
        decoded.Term.ShouldBe(10UL);
        decoded.Commit.ShouldBe(8UL);
        decoded.PrevTerm.ShouldBe(9UL);
        decoded.PrevIndex.ShouldBe(7UL);
        decoded.Entries.Count.ShouldBe(0);
        decoded.LeaderTerm.ShouldBe(0UL);
    }

    [Fact]
    public void AppendEntry_base_layout_at_correct_offsets()
    {
        // Go: server/raft.go:2693-2698 — exact layout:
        // [0..7]=leader [8..15]=term [16..23]=commit [24..31]=pterm [32..39]=pindex [40..41]=entryCount
        var ae = new RaftAppendEntryWire(
            LeaderId: "AAAAAAAA",  // 0x41 x 8
            Term: 1,
            Commit: 2,
            PrevTerm: 3,
            PrevIndex: 4,
            Entries: []);

        var bytes = ae.Encode();

        // leader bytes
        bytes[0].ShouldBe((byte)'A');
        bytes[7].ShouldBe((byte)'A');

        // term = 1 LE
        bytes[8].ShouldBe((byte)1);
        bytes[9].ShouldBe((byte)0);

        // commit = 2 LE
        bytes[16].ShouldBe((byte)2);

        // entryCount = 0
        bytes[40].ShouldBe((byte)0);
        bytes[41].ShouldBe((byte)0);
    }

    // ---------------------------------------------------------------------------
    // AppendEntry — single entry
    // ---------------------------------------------------------------------------

    [Fact]
    public void AppendEntry_single_entry_round_trip()
    {
        var data = "hello world"u8.ToArray();
        var entry = new RaftEntryWire(RaftEntryType.Normal, data);

        var ae = new RaftAppendEntryWire(
            LeaderId: "leader01",
            Term: 5,
            Commit: 3,
            PrevTerm: 4,
            PrevIndex: 2,
            Entries: [entry]);

        var decoded = RaftAppendEntryWire.Decode(ae.Encode());
        decoded.Entries.Count.ShouldBe(1);
        decoded.Entries[0].Type.ShouldBe(RaftEntryType.Normal);
        decoded.Entries[0].Data.ShouldBe(data);
    }

    [Fact]
    public void AppendEntry_entry_size_field_equals_1_plus_data_length()
    {
        // Go: server/raft.go:2702 — le.AppendUint32(buf, uint32(1+len(e.Data)))
        var data = new byte[10];
        var entry = new RaftEntryWire(RaftEntryType.PeerState, data);

        var ae = new RaftAppendEntryWire(
            LeaderId: "ld", Term: 1, Commit: 0, PrevTerm: 0, PrevIndex: 0,
            Entries: [entry]);

        var bytes = ae.Encode();

        // Entry starts at offset 42 (appendEntryBaseLen).
        // First 4 bytes are the uint32 size = 1 + 10 = 11.
        var sizeField = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(42));
        sizeField.ShouldBe(11u);

        // Byte at offset 46 is the entry type.
        bytes[46].ShouldBe((byte)RaftEntryType.PeerState);
    }

    // ---------------------------------------------------------------------------
    // AppendEntry — multiple entries
    // ---------------------------------------------------------------------------

    [Fact]
    public void AppendEntry_multiple_entries_round_trip()
    {
        var entries = new RaftEntryWire[]
        {
            new(RaftEntryType.Normal, "first"u8.ToArray()),
            new(RaftEntryType.AddPeer, "second"u8.ToArray()),
            new(RaftEntryType.RemovePeer, "third"u8.ToArray()),
        };

        var ae = new RaftAppendEntryWire(
            LeaderId: "lead0001",
            Term: 20,
            Commit: 15,
            PrevTerm: 19,
            PrevIndex: 14,
            Entries: entries);

        var decoded = RaftAppendEntryWire.Decode(ae.Encode());
        decoded.Entries.Count.ShouldBe(3);

        decoded.Entries[0].Type.ShouldBe(RaftEntryType.Normal);
        decoded.Entries[0].Data.ShouldBe("first"u8.ToArray());

        decoded.Entries[1].Type.ShouldBe(RaftEntryType.AddPeer);
        decoded.Entries[1].Data.ShouldBe("second"u8.ToArray());

        decoded.Entries[2].Type.ShouldBe(RaftEntryType.RemovePeer);
        decoded.Entries[2].Data.ShouldBe("third"u8.ToArray());
    }

    [Fact]
    public void AppendEntry_50_entries_preserve_order()
    {
        var entries = Enumerable.Range(0, 50)
            .Select(i => new RaftEntryWire(RaftEntryType.Normal, [(byte)i]))
            .ToArray();

        var ae = new RaftAppendEntryWire(
            LeaderId: "lead0001", Term: 1, Commit: 0, PrevTerm: 0, PrevIndex: 0,
            Entries: entries);

        var decoded = RaftAppendEntryWire.Decode(ae.Encode());
        decoded.Entries.Count.ShouldBe(50);

        for (var i = 0; i < 50; i++)
            decoded.Entries[i].Data[0].ShouldBe((byte)i);
    }

    [Fact]
    public void AppendEntry_entry_with_empty_data_round_trips()
    {
        var entry = new RaftEntryWire(RaftEntryType.LeaderTransfer, []);
        var ae = new RaftAppendEntryWire(
            LeaderId: "ld", Term: 1, Commit: 0, PrevTerm: 0, PrevIndex: 0,
            Entries: [entry]);

        var decoded = RaftAppendEntryWire.Decode(ae.Encode());
        decoded.Entries.Count.ShouldBe(1);
        decoded.Entries[0].Data.Length.ShouldBe(0);
        decoded.Entries[0].Type.ShouldBe(RaftEntryType.LeaderTransfer);
    }

    // ---------------------------------------------------------------------------
    // AppendEntry — leaderTerm (uvarint tail)
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2709 — buf = append(buf, lterm...)
    // Go: server/raft.go:2740-2743 — if lterm, n := binary.Uvarint(msg[ri:]); n > 0 ...
    [Theory]
    [InlineData(0UL)]
    [InlineData(1UL)]
    [InlineData(127UL)]
    [InlineData(128UL)]
    [InlineData(ulong.MaxValue)]
    public void AppendEntry_leader_term_uvarint_round_trips(ulong lterm)
    {
        var ae = new RaftAppendEntryWire(
            LeaderId: "lead0001", Term: 5, Commit: 3, PrevTerm: 4, PrevIndex: 2,
            Entries: [],
            LeaderTerm: lterm);

        var decoded = RaftAppendEntryWire.Decode(ae.Encode());
        decoded.LeaderTerm.ShouldBe(lterm);
    }

    // ---------------------------------------------------------------------------
    // AppendEntry — error cases
    // ---------------------------------------------------------------------------

    [Fact]
    public void AppendEntry_short_buffer_throws_ArgumentException()
    {
        // Buffer smaller than appendEntryBaseLen (42 bytes).
        var shortBuffer = new byte[RaftWireConstants.AppendEntryBaseLen - 1];
        Should.Throw<ArgumentException>(() => RaftAppendEntryWire.Decode(shortBuffer));
    }

    // ---------------------------------------------------------------------------
    // AppendEntryResponse
    // ---------------------------------------------------------------------------

    // Go: server/raft.go:2777-2794 — appendEntryResponse.encode()
    // Go: server/raft.go:2799-2817 — decodeAppendEntryResponse()
    [Fact]
    public void AppendEntryResponse_success_true_round_trip()
    {
        var resp = new RaftAppendEntryResponseWire(
            Term: 12, Index: 99, PeerId: "follwr01", Success: true);

        var encoded = resp.Encode();
        encoded.Length.ShouldBe(RaftWireConstants.AppendEntryResponseLen); // 25

        var decoded = RaftAppendEntryResponseWire.Decode(encoded);
        decoded.Term.ShouldBe(12UL);
        decoded.Index.ShouldBe(99UL);
        decoded.PeerId.ShouldBe("follwr01");
        decoded.Success.ShouldBeTrue();
    }

    [Fact]
    public void AppendEntryResponse_success_false_round_trip()
    {
        var resp = new RaftAppendEntryResponseWire(
            Term: 3, Index: 1, PeerId: "follwr02", Success: false);

        var decoded = RaftAppendEntryResponseWire.Decode(resp.Encode());
        decoded.Success.ShouldBeFalse();
        decoded.PeerId.ShouldBe("follwr02");
    }

    [Fact]
    public void AppendEntryResponse_success_byte_is_0_or_1()
    {
        // Go: server/raft.go:2815 — ar.success = msg[24] == 1
        var yes = new RaftAppendEntryResponseWire(Term: 1, Index: 0, PeerId: "p", Success: true);
        var no = new RaftAppendEntryResponseWire(Term: 1, Index: 0, PeerId: "p", Success: false);

        yes.Encode()[24].ShouldBe((byte)1);
        no.Encode()[24].ShouldBe((byte)0);
    }

    [Fact]
    public void AppendEntryResponse_layout_at_correct_offsets()
    {
        // Go: server/raft.go:2784-2792 — exact layout:
        // [0..7]=term [8..15]=index [16..23]=peer [24]=success
        var resp = new RaftAppendEntryResponseWire(
            Term: 1, Index: 2, PeerId: "BBBBBBBB", Success: true);
        var bytes = resp.Encode();

        bytes[0].ShouldBe((byte)1);  // term LE
        bytes[8].ShouldBe((byte)2);  // index LE
        bytes[16].ShouldBe((byte)'B'); // peer[0]
        bytes[24].ShouldBe((byte)1); // success = 1
    }

    [Fact]
    public void AppendEntryResponse_short_buffer_throws_ArgumentException()
    {
        var shortBuffer = new byte[RaftWireConstants.AppendEntryResponseLen - 1];
        Should.Throw<ArgumentException>(() => RaftAppendEntryResponseWire.Decode(shortBuffer));
    }

    [Fact]
    public void AppendEntryResponse_long_buffer_throws_ArgumentException()
    {
        var longBuffer = new byte[RaftWireConstants.AppendEntryResponseLen + 1];
        Should.Throw<ArgumentException>(() => RaftAppendEntryResponseWire.Decode(longBuffer));
    }

    [Fact]
    public void AppendEntryResponse_peer_id_truncated_to_8_bytes()
    {
        // Go: server/raft.go:2787 — copy(buf[16:16+idLen], ar.peer)
        var resp = new RaftAppendEntryResponseWire(
            Term: 1, Index: 0,
            PeerId: "verylongpeeridthatexceeds8", Success: false);

        var bytes = resp.Encode();
        var idBytes = bytes[16..24];
        System.Text.Encoding.ASCII.GetString(idBytes).ShouldBe("verylong");
    }

    // ---------------------------------------------------------------------------
    // Wire constant values
    // ---------------------------------------------------------------------------

    [Fact]
    public void Wire_constants_match_go_definitions()
    {
        // Go: server/raft.go:4558 — voteRequestLen = 24 + idLen = 32
        RaftWireConstants.VoteRequestLen.ShouldBe(32);

        // Go: server/raft.go:4737 — voteResponseLen = 8 + 8 + 1 = 17
        RaftWireConstants.VoteResponseLen.ShouldBe(17);

        // Go: server/raft.go:2660 — appendEntryBaseLen = idLen + 4*8 + 2 = 42
        RaftWireConstants.AppendEntryBaseLen.ShouldBe(42);

        // Go: server/raft.go:2757 — appendEntryResponseLen = 24 + 1 = 25
        RaftWireConstants.AppendEntryResponseLen.ShouldBe(25);

        // Go: server/raft.go:2756 — idLen = 8
        RaftWireConstants.IdLen.ShouldBe(8);
    }
}
