// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStoreRemovePartialRecovery,
//                    TestFileStoreRemoveOutOfOrderRecovery,
//                    TestFileStoreAgeLimitRecovery, TestFileStoreBitRot,
//                    TestFileStoreEraseAndNoIndexRecovery,
//                    TestFileStoreExpireMsgsOnStart,
//                    TestFileStoreRebuildStateDmapAccountingBug,
//                    TestFileStoreRecalcFirstSequenceBug,
//                    TestFileStoreFullStateBasics

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

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

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

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

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

    private FileStore CreateStore(string subdirectory, FileStoreOptions? options = null)
    {
        var dir = Path.Combine(_dir, subdirectory);
        var opts = options ?? new FileStoreOptions();
        opts.Directory = dir;
        return new FileStore(opts);
    }

    // Go: TestFileStoreRemovePartialRecovery server/filestore_test.go:1076
    [Fact]
    public async Task Remove_half_then_recover()
    {
        var subDir = "partial-recovery";

        await using (var store = CreateStore(subDir))
        {
            for (var i = 0; i < 100; i++)
                await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);

            // Remove first half.
            for (ulong i = 1; i <= 50; i++)
                await store.RemoveAsync(i, default);

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

        // Recover and verify state matches.
        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)50);
            state.FirstSeq.ShouldBe((ulong)51);
            state.LastSeq.ShouldBe((ulong)100);

            // Verify removed messages are gone.
            for (ulong i = 1; i <= 50; i++)
                (await store.LoadAsync(i, default)).ShouldBeNull();

            // Verify remaining messages are present.
            for (ulong i = 51; i <= 100; i++)
                (await store.LoadAsync(i, default)).ShouldNotBeNull();
        }
    }

    // Go: TestFileStoreRemoveOutOfOrderRecovery server/filestore_test.go:1119
    [Fact]
    public async Task Remove_evens_then_recover()
    {
        var subDir = "ooo-recovery";

        await using (var store = CreateStore(subDir))
        {
            for (var i = 0; i < 100; i++)
                await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);

            // Remove even-numbered sequences.
            for (var i = 2; i <= 100; i += 2)
                (await store.RemoveAsync((ulong)i, default)).ShouldBeTrue();

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

        // Recover and verify.
        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)50);

            // Seq 1 should exist.
            (await store.LoadAsync(1, default)).ShouldNotBeNull();

            // Even sequences should be gone.
            for (var i = 2; i <= 100; i += 2)
                (await store.LoadAsync((ulong)i, default)).ShouldBeNull();

            // Odd sequences should exist.
            for (var i = 1; i <= 99; i += 2)
                (await store.LoadAsync((ulong)i, default)).ShouldNotBeNull();
        }
    }

    // Go: TestFileStoreAgeLimitRecovery server/filestore_test.go:1183
    [Fact]
    public async Task Age_limit_recovery_expires_on_restart()
    {
        var subDir = "age-recovery";

        await using (var store = CreateStore(subDir, new FileStoreOptions { MaxAgeMs = 200 }))
        {
            for (var i = 0; i < 20; i++)
                await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);

            (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)20);
        }

        // Wait for messages to age out.
        await Task.Delay(300);

        // Reopen — expired messages should be pruned on load.
        await using (var store = CreateStore(subDir, new FileStoreOptions { MaxAgeMs = 200 }))
        {
            // Trigger prune by appending.
            await store.AppendAsync("foo", "trigger"u8.ToArray(), default);

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

    // Go: TestFileStoreEraseAndNoIndexRecovery server/filestore_test.go:1363
    [Fact]
    public async Task Remove_evens_then_recover_without_index()
    {
        var subDir = "no-index-recovery";
        var dir = Path.Combine(_dir, subDir);

        await using (var store = CreateStore(subDir))
        {
            for (var i = 0; i < 100; i++)
                await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);

            // Remove even-numbered sequences.
            for (var i = 2; i <= 100; i += 2)
                (await store.RemoveAsync((ulong)i, default)).ShouldBeTrue();

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

        // Remove the index manifest file to force a full rebuild.
        var manifestPath = Path.Combine(dir, "index.manifest.json");
        if (File.Exists(manifestPath))
            File.Delete(manifestPath);

        // Recover without index manifest.
        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)50);

            // Even sequences should still be gone.
            for (var i = 2; i <= 100; i += 2)
                (await store.LoadAsync((ulong)i, default)).ShouldBeNull();

            // Odd sequences should exist.
            for (var i = 1; i <= 99; i += 2)
                (await store.LoadAsync((ulong)i, default)).ShouldNotBeNull();
        }
    }

    // Go: TestFileStoreBitRot server/filestore_test.go:1229
    [Fact]
    public async Task Corrupted_data_file_loses_messages_but_store_recovers()
    {
        var subDir = "bitrot";
        var dir = Path.Combine(_dir, subDir);

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

        // Corrupt the data file by writing random bytes in the middle.
        var dataFile = Path.Combine(dir, "messages.jsonl");
        if (File.Exists(dataFile))
        {
            var content = File.ReadAllBytes(dataFile);
            if (content.Length > 50)
            {
                // Corrupt some bytes in the middle.
                content[content.Length / 2] = 0xFF;
                content[content.Length / 2 + 1] = 0xFE;
                File.WriteAllBytes(dataFile, content);
            }
        }

        // Recovery should not throw; it may lose some messages though.
        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            // We may lose messages due to corruption, but at least some should survive
            // if the corruption only affected one record.
            // The key point is that the store recovered without throwing.
            state.Messages.ShouldBeGreaterThanOrEqualTo((ulong)0);
        }
    }

    // Go: TestFileStoreFullStateBasics server/filestore_test.go:5461
    [Fact]
    public async Task Full_state_recovery_preserves_all_messages()
    {
        var subDir = "full-state";

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

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

        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)100);
            state.FirstSeq.ShouldBe((ulong)1);
            state.LastSeq.ShouldBe((ulong)100);

            var msg1 = await store.LoadAsync(1, default);
            msg1.ShouldNotBeNull();
            msg1!.Subject.ShouldBe("foo");

            var msg51 = await store.LoadAsync(51, default);
            msg51.ShouldNotBeNull();
            msg51!.Subject.ShouldBe("bar");
        }
    }

    // Go: TestFileStoreExpireMsgsOnStart server/filestore_test.go:3018
    [Fact]
    public async Task Expire_on_restart_with_different_maxage()
    {
        var subDir = "expire-on-start";

        // Store with no age limit.
        await using (var store = CreateStore(subDir))
        {
            for (var i = 0; i < 10; i++)
                await store.AppendAsync("foo", "Hello"u8.ToArray(), default);
        }

        await Task.Delay(100);

        // Reopen with an age limit that will expire all old messages.
        await using (var store = CreateStore(subDir, new FileStoreOptions { MaxAgeMs = 50 }))
        {
            // Trigger pruning.
            await store.AppendAsync("foo", "trigger"u8.ToArray(), default);

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

    // Go: TestFileStoreRemovePartialRecovery server/filestore_test.go:1076
    [Fact]
    public async Task Remove_then_append_then_recover()
    {
        var subDir = "rm-append-recover";

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

            await store.RemoveAsync(5, default);
            await store.AppendAsync("foo", "After remove"u8.ToArray(), default);

            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)10);
            state.LastSeq.ShouldBe((ulong)11);
        }

        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)10);
            state.LastSeq.ShouldBe((ulong)11);

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

    // Go: TestFileStoreRecalcFirstSequenceBug server/filestore_test.go:5405
    [Fact]
    public async Task Recovery_preserves_first_seq_after_removes()
    {
        var subDir = "first-seq-recovery";

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

            // Remove first 10.
            for (ulong i = 1; i <= 10; i++)
                await store.RemoveAsync(i, default);

            var state = await store.GetStateAsync(default);
            state.FirstSeq.ShouldBe((ulong)11);
        }

        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.FirstSeq.ShouldBe((ulong)11);
            state.Messages.ShouldBe((ulong)10);
        }
    }

    // Go: TestFileStoreRebuildStateDmapAccountingBug server/filestore_test.go:3692
    [Fact]
    public async Task Recovery_with_scattered_deletes_preserves_count()
    {
        var subDir = "scattered-deletes";

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

            // Delete scattered: every 3rd.
            for (var i = 3; i <= 50; i += 3)
                await store.RemoveAsync((ulong)i, default);

            var expectedCount = 50 - (50 / 3);

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

        await using (var store = CreateStore(subDir))
        {
            var expectedCount = 50 - (50 / 3);
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)expectedCount);
        }
    }

    // Go: TestFileStoreBasicWriteMsgsAndRestore server/filestore_test.go:181
    [Fact]
    public async Task Recovery_preserves_message_payloads()
    {
        var subDir = "payload-recovery";

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

        await using (var store = CreateStore(subDir))
        {
            for (ulong i = 1; i <= 10; i++)
            {
                var msg = await store.LoadAsync(i, default);
                msg.ShouldNotBeNull();
                msg!.Subject.ShouldBe("foo");
                var expected = Encoding.UTF8.GetBytes($"message-{i - 1}");
                msg.Payload.ToArray().ShouldBe(expected);
            }
        }
    }

    // Go: TestFileStoreBasicWriteMsgsAndRestore server/filestore_test.go:181
    [Fact]
    public async Task Recovery_preserves_subjects()
    {
        var subDir = "subject-recovery";

        await using (var store = CreateStore(subDir))
        {
            await store.AppendAsync("alpha", "one"u8.ToArray(), default);
            await store.AppendAsync("beta", "two"u8.ToArray(), default);
            await store.AppendAsync("gamma", "three"u8.ToArray(), default);
        }

        await using (var store = CreateStore(subDir))
        {
            var msg1 = await store.LoadAsync(1, default);
            msg1.ShouldNotBeNull();
            msg1!.Subject.ShouldBe("alpha");

            var msg2 = await store.LoadAsync(2, default);
            msg2.ShouldNotBeNull();
            msg2!.Subject.ShouldBe("beta");

            var msg3 = await store.LoadAsync(3, default);
            msg3.ShouldNotBeNull();
            msg3!.Subject.ShouldBe("gamma");
        }
    }

    // Go: TestFileStoreRemoveOutOfOrderRecovery server/filestore_test.go:1119
    [Fact]
    public async Task Recovery_with_large_message_count()
    {
        var subDir = "large-recovery";

        await using (var store = CreateStore(subDir))
        {
            for (var i = 0; i < 500; i++)
                await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i:D4}"), default);
        }

        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)500);
            state.FirstSeq.ShouldBe((ulong)1);
            state.LastSeq.ShouldBe((ulong)500);
        }
    }
}
