// Go ref: filestore.go:10599 (_writeFullState)
// AtomicFileWriter wraps the write-to-temp-then-rename pattern used by
// Go's fileStore._writeFullState to guarantee crash-safe state persistence.

using NATS.Server.JetStream.Storage;

namespace NATS.Server.JetStream.Tests.JetStream.Storage;

public sealed class AtomicFileWriterTests : IDisposable
{
    private readonly DirectoryInfo _dir;

    public AtomicFileWriterTests()
    {
        _dir = Directory.CreateTempSubdirectory("atomic_writer_tests_");
    }

    public void Dispose() => _dir.Delete(recursive: true);

    private string TempPath(string name) => Path.Combine(_dir.FullName, name);

    // -------------------------------------------------------------------------
    // byte[] overload
    // -------------------------------------------------------------------------

    [Fact]
    public async Task WriteAtomicallyAsync_creates_file()
    {
        var path = TempPath("state.json");
        var data = "{ \"seq\": 1 }"u8.ToArray();

        await AtomicFileWriter.WriteAtomicallyAsync(path, data);

        File.Exists(path).ShouldBeTrue();
        var written = await File.ReadAllBytesAsync(path);
        written.ShouldBe(data);
    }

    [Fact]
    public async Task WriteAtomicallyAsync_no_temp_file_remains()
    {
        var path = TempPath("state.json");
        var data = "hello world"u8.ToArray();

        await AtomicFileWriter.WriteAtomicallyAsync(path, data);

        // No .tmp file should remain after a successful write.
        // The temp file uses a random component ({path}.{random}.tmp) so check by extension.
        _dir.EnumerateFiles("*.tmp").ShouldBeEmpty();
    }

    [Fact]
    public async Task WriteAtomicallyAsync_overwrites_existing()
    {
        var path = TempPath("state.json");

        await AtomicFileWriter.WriteAtomicallyAsync(path, "first"u8.ToArray());
        await AtomicFileWriter.WriteAtomicallyAsync(path, "second"u8.ToArray());

        var written = await File.ReadAllTextAsync(path);
        written.ShouldBe("second");
    }

    [Fact]
    public async Task WriteAtomicallyAsync_concurrent_writes_are_safe()
    {
        // Multiple concurrent writes to the same file must not corrupt it.
        // Each write uses a unique payload; after all writes complete the
        // file must contain exactly one of the payloads (no partial data).
        var path = TempPath("concurrent.json");
        const int concurrency = 20;

        var tasks = Enumerable.Range(0, concurrency).Select(i =>
            AtomicFileWriter.WriteAtomicallyAsync(path, System.Text.Encoding.UTF8.GetBytes($"payload-{i:D3}")));

        await Task.WhenAll(tasks);

        // File must exist and contain exactly one complete payload.
        File.Exists(path).ShouldBeTrue();
        var content = await File.ReadAllTextAsync(path);
        content.ShouldMatch(@"^payload-\d{3}$");
    }

    // -------------------------------------------------------------------------
    // ReadOnlyMemory<byte> overload
    // -------------------------------------------------------------------------

    [Fact]
    public async Task WriteAtomicallyAsync_memory_overload_creates_file()
    {
        var path = TempPath("state_mem.json");
        ReadOnlyMemory<byte> data = "{ \"seq\": 42 }"u8.ToArray();

        await AtomicFileWriter.WriteAtomicallyAsync(path, data);

        File.Exists(path).ShouldBeTrue();
        var written = await File.ReadAllBytesAsync(path);
        written.ShouldBe(data.ToArray());
    }

    [Fact]
    public async Task WriteAtomicallyAsync_memory_overload_no_temp_file_remains()
    {
        var path = TempPath("state_mem.json");
        ReadOnlyMemory<byte> data = "memory data"u8.ToArray();

        await AtomicFileWriter.WriteAtomicallyAsync(path, data);

        // The temp file uses a random component ({path}.{random}.tmp) so check by extension.
        _dir.EnumerateFiles("*.tmp").ShouldBeEmpty();
    }

    [Fact]
    public async Task WriteAtomicallyAsync_memory_overload_overwrites_existing()
    {
        var path = TempPath("state_mem.json");

        await AtomicFileWriter.WriteAtomicallyAsync(path, (ReadOnlyMemory<byte>)"first"u8.ToArray());
        await AtomicFileWriter.WriteAtomicallyAsync(path, (ReadOnlyMemory<byte>)"second"u8.ToArray());

        var written = await File.ReadAllTextAsync(path);
        written.ShouldBe("second");
    }

    [Fact]
    public async Task WriteAtomicallyAsync_writes_empty_data()
    {
        var path = TempPath("empty.json");

        await AtomicFileWriter.WriteAtomicallyAsync(path, Array.Empty<byte>());

        File.Exists(path).ShouldBeTrue();
        var written = await File.ReadAllBytesAsync(path);
        written.ShouldBeEmpty();
    }

    [Fact]
    public async Task WriteAtomicallyAsync_writes_large_payload()
    {
        var path = TempPath("large.bin");
        var data = new byte[256 * 1024]; // 256 KB
        Random.Shared.NextBytes(data);

        await AtomicFileWriter.WriteAtomicallyAsync(path, data);

        var written = await File.ReadAllBytesAsync(path);
        written.ShouldBe(data);
    }
}
