using System.Text.Json;
using NATS.Server.Raft;

namespace NATS.Server.Raft.Tests.Raft;

/// <summary>
/// Ported from Go: TestNRGAppendEntryEncode in golang/nats-server/server/raft_test.go
/// Tests append entry serialization/deserialization and log entry mechanics.
/// The Go test validates binary encode/decode of appendEntry; the .NET equivalent
/// validates JSON round-trip of RaftLogEntry and log persistence.
/// </summary>
public class RaftAppendEntryTests
{
    [Fact]
    public void Append_entry_encode_decode_round_trips()
    {
        // Reference: TestNRGAppendEntryEncode — test entry serialization.
        // In .NET the RaftLogEntry is a sealed record serialized via JSON.
        var original = new RaftLogEntry(Index: 1, Term: 1, Command: "test-command");

        var json = JsonSerializer.Serialize(original);
        json.ShouldNotBeNullOrWhiteSpace();

        var decoded = JsonSerializer.Deserialize<RaftLogEntry>(json);
        decoded.ShouldNotBeNull();
        decoded.Index.ShouldBe(original.Index);
        decoded.Term.ShouldBe(original.Term);
        decoded.Command.ShouldBe(original.Command);
    }

    [Fact]
    public void Append_entry_with_empty_command_round_trips()
    {
        // Reference: TestNRGAppendEntryEncode — Go test encodes entry with nil data.
        var original = new RaftLogEntry(Index: 5, Term: 2, Command: string.Empty);

        var json = JsonSerializer.Serialize(original);
        var decoded = JsonSerializer.Deserialize<RaftLogEntry>(json);
        decoded.ShouldNotBeNull();
        decoded.Index.ShouldBe(5);
        decoded.Term.ShouldBe(2);
        decoded.Command.ShouldBe(string.Empty);
    }

    [Fact]
    public void Multiple_entries_encode_decode_preserves_order()
    {
        // Reference: TestNRGAppendEntryEncode — Go test encodes multiple entries.
        var entries = Enumerable.Range(0, 100)
            .Select(i => new RaftLogEntry(Index: i + 1, Term: 1, Command: $"cmd-{i}"))
            .ToList();

        var json = JsonSerializer.Serialize(entries);
        var decoded = JsonSerializer.Deserialize<List<RaftLogEntry>>(json);

        decoded.ShouldNotBeNull();
        decoded.Count.ShouldBe(100);

        for (var i = 0; i < 100; i++)
        {
            decoded[i].Index.ShouldBe(i + 1);
            decoded[i].Term.ShouldBe(1);
            decoded[i].Command.ShouldBe($"cmd-{i}");
        }
    }

    [Fact]
    public void Log_append_assigns_sequential_indices()
    {
        var log = new RaftLog();

        var e1 = log.Append(term: 1, command: "first");
        var e2 = log.Append(term: 1, command: "second");
        var e3 = log.Append(term: 2, command: "third");

        e1.Index.ShouldBe(1);
        e2.Index.ShouldBe(2);
        e3.Index.ShouldBe(3);

        log.Entries.Count.ShouldBe(3);
        log.Entries[0].Command.ShouldBe("first");
        log.Entries[1].Command.ShouldBe("second");
        log.Entries[2].Command.ShouldBe("third");
    }

    [Fact]
    public void Log_append_replicated_deduplicates_by_index()
    {
        var log = new RaftLog();
        var entry = new RaftLogEntry(Index: 1, Term: 1, Command: "cmd");

        log.AppendReplicated(entry);
        log.AppendReplicated(entry); // duplicate should be ignored

        log.Entries.Count.ShouldBe(1);
    }

    [Fact]
    public void Log_replace_with_snapshot_clears_entries_and_resets_base()
    {
        // Reference: TestNRGSnapshotAndRestart — snapshot replaces log.
        var log = new RaftLog();
        log.Append(term: 1, command: "a");
        log.Append(term: 1, command: "b");
        log.Append(term: 1, command: "c");
        log.Entries.Count.ShouldBe(3);

        var snapshot = new RaftSnapshot
        {
            LastIncludedIndex = 3,
            LastIncludedTerm = 1,
        };

        log.ReplaceWithSnapshot(snapshot);
        log.Entries.Count.ShouldBe(0);

        // After snapshot, new entries should start at index 4.
        var e = log.Append(term: 2, command: "post-snapshot");
        e.Index.ShouldBe(4);
    }

    [Fact]
    public async Task Log_persist_and_reload_round_trips()
    {
        // Reference: TestNRGSnapshotAndRestart — persistence round-trip.
        var dir = Path.Combine(Path.GetTempPath(), $"nats-raft-log-test-{Guid.NewGuid():N}");
        var logPath = Path.Combine(dir, "log.json");

        try
        {
            var log = new RaftLog();
            log.Append(term: 1, command: "alpha");
            log.Append(term: 1, command: "beta");
            log.Append(term: 2, command: "gamma");

            await log.PersistAsync(logPath, CancellationToken.None);
            File.Exists(logPath).ShouldBeTrue();

            var reloaded = await RaftLog.LoadAsync(logPath, CancellationToken.None);
            reloaded.Entries.Count.ShouldBe(3);
            reloaded.Entries[0].Index.ShouldBe(1);
            reloaded.Entries[0].Term.ShouldBe(1);
            reloaded.Entries[0].Command.ShouldBe("alpha");
            reloaded.Entries[1].Command.ShouldBe("beta");
            reloaded.Entries[2].Command.ShouldBe("gamma");
            reloaded.Entries[2].Term.ShouldBe(2);
        }
        finally
        {
            if (Directory.Exists(dir))
                Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public async Task Log_load_returns_empty_for_nonexistent_path()
    {
        var logPath = Path.Combine(Path.GetTempPath(), $"nats-raft-noexist-{Guid.NewGuid():N}", "log.json");

        var log = await RaftLog.LoadAsync(logPath, CancellationToken.None);
        log.Entries.Count.ShouldBe(0);
    }

    [Fact]
    public void Entry_record_equality_holds_for_identical_values()
    {
        // RaftLogEntry is a sealed record — structural equality should work.
        var a = new RaftLogEntry(Index: 1, Term: 1, Command: "cmd");
        var b = new RaftLogEntry(Index: 1, Term: 1, Command: "cmd");
        a.ShouldBe(b);

        var c = new RaftLogEntry(Index: 2, Term: 1, Command: "cmd");
        a.ShouldNotBe(c);
    }

    [Fact]
    public void Entry_term_is_preserved_through_append()
    {
        var log = new RaftLog();
        var e1 = log.Append(term: 3, command: "term3-entry");
        var e2 = log.Append(term: 5, command: "term5-entry");

        e1.Term.ShouldBe(3);
        e2.Term.ShouldBe(5);
        log.Entries[0].Term.ShouldBe(3);
        log.Entries[1].Term.ShouldBe(5);
    }
}
