// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStoreEncrypted,
//                    TestFileStoreRestoreEncryptedWithNoKeyFuncFails,
//                    TestFileStoreDoubleCompactWithWriteInBetweenEncryptedBug,
//                    TestFileStoreEncryptedKeepIndexNeedBekResetBug,
//                    TestFileStoreShortIndexWriteBug (encryption variant)

using System.Text;
using NATS.Server.JetStream.Storage;

namespace NATS.Server.Tests.JetStream.Storage;

public sealed class FileStoreEncryptionTests : IDisposable
{
    private readonly string _dir;

    public FileStoreEncryptionTests()
    {
        _dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-enc-{Guid.NewGuid():N}");
        Directory.CreateDirectory(_dir);
    }

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

    private static byte[] TestKey => "nats-encryption-key-for-test!!"u8.ToArray();

    private FileStore CreateStore(string subdirectory, bool encrypt = true, byte[]? key = null)
    {
        var dir = Path.Combine(_dir, subdirectory);
        return new FileStore(new FileStoreOptions
        {
            Directory = dir,
            EnableEncryption = encrypt,
            EncryptionKey = key ?? TestKey,
        });
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_store_and_load()
    {
        await using var store = CreateStore("enc-basic");

        const string subject = "foo";
        var payload = "aes ftw"u8.ToArray();

        for (var i = 0; i < 50; i++)
            await store.AppendAsync(subject, payload, default);

        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)50);

        var msg = await store.LoadAsync(10, default);
        msg.ShouldNotBeNull();
        msg!.Subject.ShouldBe(subject);
        msg.Payload.ToArray().ShouldBe(payload);
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4228
    [Fact]
    public async Task Encrypted_store_and_recover()
    {
        var subDir = "enc-recover";

        await using (var store = CreateStore(subDir))
        {
            for (var i = 0; i < 50; i++)
                await store.AppendAsync("foo", "aes ftw"u8.ToArray(), default);
        }

        // Reopen with the same key.
        await using (var store = CreateStore(subDir))
        {
            var msg = await store.LoadAsync(10, default);
            msg.ShouldNotBeNull();
            msg!.Payload.ToArray().ShouldBe("aes ftw"u8.ToArray());

            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)50);
        }
    }

    // Go: TestFileStoreRestoreEncryptedWithNoKeyFuncFails server/filestore_test.go:5134
    [Fact]
    public async Task Encrypted_data_without_key_throws_on_load()
    {
        var subDir = "enc-no-key";
        var dir = Path.Combine(_dir, subDir);

        // Store with encryption.
        await using (var store = CreateStore(subDir))
        {
            await store.AppendAsync("foo", "secret data"u8.ToArray(), default);
        }

        // Reopen with a wrong key. The FileStore constructor calls LoadExisting()
        // which calls RestorePayload(), and that throws InvalidDataException when
        // the envelope key-hash does not match the configured key.
        var createWithWrongKey = () => new FileStore(new FileStoreOptions
        {
            Directory = dir,
            EnableEncryption = true,
            EncryptionKey = "wrong-key-wrong-key-wrong-key!!"u8.ToArray(),
            EnablePayloadIntegrityChecks = true,
        });

        Should.Throw<InvalidDataException>(createWithWrongKey);
        await Task.CompletedTask;
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_store_remove_and_reload()
    {
        await using var store = CreateStore("enc-remove");

        for (var i = 0; i < 10; i++)
            await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);

        await store.RemoveAsync(5, default);

        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)9);

        (await store.LoadAsync(5, default)).ShouldBeNull();
        (await store.LoadAsync(6, default)).ShouldNotBeNull();
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_purge_and_continue()
    {
        await using var store = CreateStore("enc-purge");

        for (var i = 0; i < 10; i++)
            await store.AppendAsync("foo", "data"u8.ToArray(), default);

        await store.PurgeAsync(default);
        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)0);

        var seq = await store.AppendAsync("foo", "after purge"u8.ToArray(), default);
        seq.ShouldBeGreaterThan((ulong)0);

        var msg = await store.LoadAsync(seq, default);
        msg.ShouldNotBeNull();
        msg!.Payload.ToArray().ShouldBe("after purge"u8.ToArray());
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_snapshot_and_restore()
    {
        await using var store = CreateStore("enc-snap-src");

        for (var i = 0; i < 20; i++)
            await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);

        var snap = await store.CreateSnapshotAsync(default);
        snap.Length.ShouldBeGreaterThan(0);

        await using var restored = CreateStore("enc-snap-dst");
        await restored.RestoreSnapshotAsync(snap, default);

        var srcState = await store.GetStateAsync(default);
        var dstState = await restored.GetStateAsync(default);
        dstState.Messages.ShouldBe(srcState.Messages);

        for (ulong i = 1; i <= srcState.Messages; i++)
        {
            var original = await store.LoadAsync(i, default);
            var copy = await restored.LoadAsync(i, default);
            copy.ShouldNotBeNull();
            copy!.Payload.ToArray().ShouldBe(original!.Payload.ToArray());
        }
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_large_payload()
    {
        await using var store = CreateStore("enc-large");

        var payload = new byte[8192];
        Random.Shared.NextBytes(payload);

        await store.AppendAsync("foo", payload, default);

        var msg = await store.LoadAsync(1, default);
        msg.ShouldNotBeNull();
        msg!.Payload.ToArray().ShouldBe(payload);
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_binary_payload_round_trips()
    {
        await using var store = CreateStore("enc-binary");

        // All byte values.
        var payload = new byte[256];
        for (var i = 0; i < 256; i++)
            payload[i] = (byte)i;

        await store.AppendAsync("foo", payload, default);

        var msg = await store.LoadAsync(1, default);
        msg.ShouldNotBeNull();
        msg!.Payload.ToArray().ShouldBe(payload);
    }

    // Go: TestFileStoreEncrypted server/filestore_test.go:4204
    [Fact]
    public async Task Encrypted_empty_payload()
    {
        await using var store = CreateStore("enc-empty");

        await store.AppendAsync("foo", ReadOnlyMemory<byte>.Empty, default);

        var msg = await store.LoadAsync(1, default);
        msg.ShouldNotBeNull();
        msg!.Payload.Length.ShouldBe(0);
    }

    // Go: TestFileStoreDoubleCompactWithWriteInBetweenEncryptedBug server/filestore_test.go:3924
    [Fact(Skip = "Compact not yet implemented in .NET FileStore")]
    public async Task Encrypted_double_compact_with_write_in_between()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreEncryptedKeepIndexNeedBekResetBug server/filestore_test.go:3956
    [Fact(Skip = "Block encryption key reset not yet implemented in .NET FileStore")]
    public async Task Encrypted_keep_index_bek_reset()
    {
        await Task.CompletedTask;
    }

    // Verify encryption with no-op key (empty key) does not crash.
    [Fact]
    public async Task Encrypted_with_empty_key_is_noop()
    {
        var dir = Path.Combine(_dir, "enc-noop");
        await using var store = new FileStore(new FileStoreOptions
        {
            Directory = dir,
            EnableEncryption = true,
            EncryptionKey = [],
        });

        await store.AppendAsync("foo", "data"u8.ToArray(), default);

        var msg = await store.LoadAsync(1, default);
        msg.ShouldNotBeNull();
        msg!.Payload.ToArray().ShouldBe("data"u8.ToArray());
    }

    // Verify data at rest is not plaintext when encrypted.
    [Fact]
    public async Task Encrypted_data_not_plaintext_on_disk()
    {
        var subDir = "enc-disk-check";
        var dir = Path.Combine(_dir, subDir);

        await using (var store = CreateStore(subDir))
        {
            await store.AppendAsync("foo", "THIS IS SENSITIVE DATA"u8.ToArray(), default);
        }

        // Read the raw data file and verify the plaintext payload does not appear.
        var dataFile = Path.Combine(dir, "messages.jsonl");
        if (File.Exists(dataFile))
        {
            var raw = File.ReadAllText(dataFile);
            // The payload is base64-encoded after encryption, so the original
            // plaintext string should not appear verbatim.
            raw.ShouldNotContain("THIS IS SENSITIVE DATA");
        }
    }
}
