using NATS.Server.Raft;

// Go reference: server/raft.go (WAL binary format, compaction, CRC integrity)

namespace NATS.Server.Raft.Tests.Raft;

public class RaftWalTests : IDisposable
{
    private readonly string _root;

    public RaftWalTests()
    {
        _root = Path.Combine(Path.GetTempPath(), $"nats-wal-{Guid.NewGuid():N}");
        Directory.CreateDirectory(_root);
    }

    public void Dispose()
    {
        if (Directory.Exists(_root))
            Directory.Delete(_root, recursive: true);
    }

    // Go reference: server/raft.go WAL append + recover
    [Fact]
    public async Task Wal_persists_and_recovers_entries()
    {
        var walPath = Path.Combine(_root, "raft.wal");

        // Write entries
        {
            using var wal = new RaftWal(walPath);
            await wal.AppendAsync(new RaftLogEntry(1, 1, "cmd-1"));
            await wal.AppendAsync(new RaftLogEntry(2, 1, "cmd-2"));
            await wal.AppendAsync(new RaftLogEntry(3, 2, "cmd-3"));
            await wal.SyncAsync();
        }

        // Recover
        using var recovered = RaftWal.Load(walPath);
        var entries = recovered.Entries;
        entries.Count.ShouldBe(3);
        entries[0].Index.ShouldBe(1);
        entries[0].Term.ShouldBe(1);
        entries[0].Command.ShouldBe("cmd-1");
        entries[2].Index.ShouldBe(3);
        entries[2].Term.ShouldBe(2);
    }

    // Go reference: server/raft.go compactLog
    [Fact]
    public async Task Wal_compact_removes_old_entries()
    {
        var walPath = Path.Combine(_root, "compact.wal");

        using var wal = new RaftWal(walPath);
        for (int i = 1; i <= 10; i++)
            await wal.AppendAsync(new RaftLogEntry(i, 1, $"cmd-{i}"));
        await wal.SyncAsync();

        await wal.CompactAsync(5); // remove entries 1-5

        using var recovered = RaftWal.Load(walPath);
        recovered.Entries.Count.ShouldBe(5);
        recovered.Entries.First().Index.ShouldBe(6);
    }

    // Go reference: server/raft.go WAL crash-truncation tolerance
    [Fact]
    public async Task Wal_handles_truncated_file()
    {
        var walPath = Path.Combine(_root, "truncated.wal");

        {
            using var wal = new RaftWal(walPath);
            await wal.AppendAsync(new RaftLogEntry(1, 1, "good-entry"));
            await wal.AppendAsync(new RaftLogEntry(2, 1, "will-be-truncated"));
            await wal.SyncAsync();
        }

        // Truncate last few bytes to simulate crash
        using (var fs = File.OpenWrite(walPath))
            fs.SetLength(fs.Length - 3);

        using var recovered = RaftWal.Load(walPath);
        recovered.Entries.Count.ShouldBe(1);
        recovered.Entries.First().Command.ShouldBe("good-entry");
    }

    // Go reference: server/raft.go storeMeta (term + votedFor persistence)
    [Fact]
    public async Task RaftNode_persists_term_and_vote()
    {
        var dir = Path.Combine(_root, "node-persist");
        Directory.CreateDirectory(dir);

        {
            using var node = new RaftNode("n1", persistDirectory: dir);
            node.TermState.CurrentTerm = 5;
            node.TermState.VotedFor = "n2";
            await node.PersistAsync(default);
        }

        using var recovered = new RaftNode("n1", persistDirectory: dir);
        await recovered.LoadPersistedStateAsync(default);
        recovered.Term.ShouldBe(5);
        recovered.TermState.VotedFor.ShouldBe("n2");
    }

    // Go reference: server/raft.go WAL empty file edge case
    [Fact]
    public async Task Wal_empty_file_loads_no_entries()
    {
        var walPath = Path.Combine(_root, "empty.wal");

        {
            using var wal = new RaftWal(walPath);
            await wal.SyncAsync();
        }

        using var recovered = RaftWal.Load(walPath);
        recovered.Entries.Count.ShouldBe(0);
    }

    // Go reference: server/raft.go WAL CRC integrity check
    [Fact]
    public async Task Wal_crc_validates_record_integrity()
    {
        var walPath = Path.Combine(_root, "crc.wal");

        {
            using var wal = new RaftWal(walPath);
            await wal.AppendAsync(new RaftLogEntry(1, 1, "valid"));
            await wal.AppendAsync(new RaftLogEntry(2, 1, "also-valid"));
            await wal.SyncAsync();
        }

        // Corrupt one byte in the tail of the file (inside the second record)
        var bytes = File.ReadAllBytes(walPath);
        bytes[^5] ^= 0xFF;
        File.WriteAllBytes(walPath, bytes);

        // Load should recover exactly the first record, stopping at the corrupt second
        using var recovered = RaftWal.Load(walPath);
        recovered.Entries.Count.ShouldBe(1);
        recovered.Entries.First().Command.ShouldBe("valid");
    }
}
