// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStorePurge, TestFileStoreCompact,
//                    TestFileStoreCompactLastPlusOne, TestFileStoreCompactMsgCountBug,
//                    TestFileStorePurgeExWithSubject, TestFileStorePurgeExKeepOneBug,
//                    TestFileStorePurgeExNoTombsOnBlockRemoval,
//                    TestFileStoreStreamTruncate

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

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

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

    public FileStorePurgeTests()
    {
        _dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-purge-{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: TestFileStorePurge server/filestore_test.go:709
    [Fact]
    public async Task Purge_removes_all_messages()
    {
        await using var store = CreateStore("purge-all");

        for (var i = 0; i < 100; i++)
            await store.AppendAsync("foo", new byte[128], default);

        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)100);

        await store.PurgeAsync(default);

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

    // Go: TestFileStorePurge server/filestore_test.go:740
    [Fact]
    public async Task Purge_recovers_same_state_after_restart()
    {
        var subDir = "purge-restart";

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

            await store.PurgeAsync(default);
        }

        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)0);
            state.Bytes.ShouldBe((ulong)0);
        }
    }

    // Go: TestFileStorePurge server/filestore_test.go:776
    [Fact]
    public async Task Store_after_purge_works()
    {
        await using var store = CreateStore("purge-then-store");

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

        await store.PurgeAsync(default);

        // New messages after purge.
        for (var i = 0; i < 10; i++)
        {
            var seq = await store.AppendAsync("foo", "After purge"u8.ToArray(), default);
            seq.ShouldBeGreaterThan((ulong)0);
        }

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

    // Go: TestFileStoreCompact server/filestore_test.go:822
    [Fact]
    public async Task Compact_removes_messages_below_sequence()
    {
        await using var store = CreateStore("compact-below-seq");

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

        // Compact removes all messages with seq < 5, leaving seqs 5-10 (6 messages).
        var removed = store.Compact(5);
        removed.ShouldBe((ulong)4);

        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)6);
        state.FirstSeq.ShouldBe((ulong)5);
        state.LastSeq.ShouldBe((ulong)10);

        // Seqs 1-4 must be gone.
        for (ulong seq = 1; seq <= 4; seq++)
            (await store.LoadAsync(seq, default)).ShouldBeNull();

        // Seqs 5-10 must still be present.
        for (ulong seq = 5; seq <= 10; seq++)
            (await store.LoadAsync(seq, default)).ShouldNotBeNull();
    }

    // Go: TestFileStoreCompact server/filestore_test.go:851
    [Fact]
    public async Task Compact_beyond_last_seq_resets_first()
    {
        await using var store = CreateStore("compact-beyond-last");

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

        // Compact at seq 100 (beyond last seq 10) removes all messages.
        var removed = store.Compact(100);
        removed.ShouldBe((ulong)10);

        var apiState = await store.GetStateAsync(default);
        apiState.Messages.ShouldBe((ulong)0);

        // FastState / State() should report _first watermark = 100.
        var state = store.State();
        state.Msgs.ShouldBe((ulong)0);
        state.FirstSeq.ShouldBe((ulong)100);
    }

    // Go: TestFileStoreCompact server/filestore_test.go:862
    [Fact]
    public async Task Compact_recovers_after_restart()
    {
        var subDir = "compact-restart";

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

            store.Compact(5);
        }

        // Reopen the same directory and verify state is preserved.
        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)6);
            state.FirstSeq.ShouldBe((ulong)5);
            state.LastSeq.ShouldBe((ulong)10);
        }
    }

    // Go: TestFileStoreCompactLastPlusOne server/filestore_test.go:875
    [Fact]
    public async Task Compact_last_plus_one_clears_all()
    {
        await using var store = CreateStore("compact-last-plus-one");

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

        var lastSeq = (await store.GetStateAsync(default)).LastSeq;
        lastSeq.ShouldBe((ulong)10);

        // Compact at lastSeq+1 removes all messages.
        var removed = store.Compact(lastSeq + 1);
        removed.ShouldBe((ulong)10);

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

    // Go: TestFileStoreCompactMsgCountBug server/filestore_test.go:916
    [Fact]
    public async Task Compact_with_prior_deletes_counts_correctly()
    {
        await using var store = CreateStore("compact-prior-deletes");

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

        // Remove seq 3 and 7 before compacting.
        await store.RemoveAsync(3, default);
        await store.RemoveAsync(7, default);

        // Compact at seq 5: removes seqs < 5 that still exist (1, 2, 4 — seq 3 already gone).
        store.Compact(5);

        // Remaining: seqs 5, 6, 8, 9, 10 (seq 7 was already deleted).
        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)5);
        state.FirstSeq.ShouldBe((ulong)5);
        state.LastSeq.ShouldBe((ulong)10);

        // Confirm seq 5, 6, 8, 9, 10 are loadable; 3, 7 are gone.
        (await store.LoadAsync(5, default)).ShouldNotBeNull();
        (await store.LoadAsync(6, default)).ShouldNotBeNull();
        (await store.LoadAsync(8, default)).ShouldNotBeNull();
        (await store.LoadAsync(9, default)).ShouldNotBeNull();
        (await store.LoadAsync(10, default)).ShouldNotBeNull();
        (await store.LoadAsync(3, default)).ShouldBeNull();
        (await store.LoadAsync(7, default)).ShouldBeNull();
    }

    // Go: TestFileStoreStreamTruncate server/filestore_test.go:991
    [Fact]
    public async Task Truncate_removes_messages_after_sequence()
    {
        await using var store = CreateStore("truncate-after-seq");

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

        // Truncate at seq 5: removes seqs > 5, leaving seqs 1-5.
        store.Truncate(5);

        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)5);
        state.FirstSeq.ShouldBe((ulong)1);
        state.LastSeq.ShouldBe((ulong)5);

        // Seqs 6-10 must be gone.
        for (ulong seq = 6; seq <= 10; seq++)
            (await store.LoadAsync(seq, default)).ShouldBeNull();

        // Seqs 1-5 must still be present.
        for (ulong seq = 1; seq <= 5; seq++)
            (await store.LoadAsync(seq, default)).ShouldNotBeNull();
    }

    // Go: TestFileStoreStreamTruncate server/filestore_test.go:1025
    [Fact]
    public async Task Truncate_with_interior_deletes()
    {
        await using var store = CreateStore("truncate-interior-deletes");

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

        // Remove seq 3 and 7 before truncating.
        await store.RemoveAsync(3, default);
        await store.RemoveAsync(7, default);

        // Truncate at seq 5: removes seqs > 5 that still exist (6, 8, 9, 10 — seq 7 already gone).
        store.Truncate(5);

        // Remaining: seqs 1, 2, 4, 5 (seq 3 was already deleted).
        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)4);
        state.LastSeq.ShouldBe((ulong)5);

        (await store.LoadAsync(1, default)).ShouldNotBeNull();
        (await store.LoadAsync(2, default)).ShouldNotBeNull();
        (await store.LoadAsync(3, default)).ShouldBeNull();
        (await store.LoadAsync(4, default)).ShouldNotBeNull();
        (await store.LoadAsync(5, default)).ShouldNotBeNull();
        (await store.LoadAsync(6, default)).ShouldBeNull();
        (await store.LoadAsync(7, default)).ShouldBeNull();
    }

    // Go: TestFileStorePurgeExWithSubject server/filestore_test.go:3743
    [Fact]
    public async Task PurgeEx_with_subject_removes_matching()
    {
        await using var store = CreateStore("purgeex-subject");

        // Interleave "foo" and "bar" messages.
        for (var i = 0; i < 5; i++)
        {
            await store.AppendAsync("foo", "foo-data"u8.ToArray(), default);
            await store.AppendAsync("bar", "bar-data"u8.ToArray(), default);
        }

        var before = await store.GetStateAsync(default);
        before.Messages.ShouldBe((ulong)10);

        // PurgeEx with subject="foo", seq=0, keep=0: removes all "foo" messages.
        var removed = store.PurgeEx("foo", 0, 0);
        removed.ShouldBe((ulong)5);

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

        // All remaining messages should be on "bar".
        var messages = await store.ListAsync(default);
        messages.Count.ShouldBe(5);
        foreach (var msg in messages)
            msg.Subject.ShouldBe("bar");
    }

    // Go: TestFileStorePurgeExKeepOneBug server/filestore_test.go:3382
    [Fact]
    public async Task PurgeEx_keep_one_preserves_last()
    {
        await using var store = CreateStore("purgeex-keep-one");

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

        lastSeq.ShouldBe((ulong)5);

        // PurgeEx with keep=1: should remove 4 messages, keeping only the last one.
        var removed = store.PurgeEx("foo", 0, 1);
        removed.ShouldBe((ulong)4);

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

        // The remaining message must be the one with the highest sequence.
        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(1);
        remaining[0].Sequence.ShouldBe(lastSeq);
    }

    // Go: TestFileStorePurgeExNoTombsOnBlockRemoval server/filestore_test.go:3823
    [Fact]
    public async Task PurgeEx_no_tombstones_on_block_removal()
    {
        await using var store = CreateStore("purgeex-no-tombs");

        // Store messages on "foo" and "bar".
        for (var i = 0; i < 5; i++)
            await store.AppendAsync("foo", "foo-data"u8.ToArray(), default);

        var barSeqs = new List<ulong>();
        for (var i = 0; i < 5; i++)
            barSeqs.Add(await store.AppendAsync("bar", "bar-data"u8.ToArray(), default));

        // PurgeEx removes all "foo" messages.
        store.PurgeEx("foo", 0, 0);

        // "bar" messages must still be loadable and state must be consistent.
        foreach (var seq in barSeqs)
        {
            var msg = await store.LoadAsync(seq, default);
            msg.ShouldNotBeNull();
            msg!.Subject.ShouldBe("bar");
        }

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

    // Go: TestFileStorePurge server/filestore_test.go:709
    [Fact]
    public async Task Purge_then_list_returns_empty()
    {
        await using var store = CreateStore("purge-list");

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

        await store.PurgeAsync(default);

        var messages = await store.ListAsync(default);
        messages.Count.ShouldBe(0);
    }

    // Go: TestFileStorePurge server/filestore_test.go:709
    [Fact]
    public async Task Multiple_purges_are_safe()
    {
        await using var store = CreateStore("multi-purge");

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

        await store.PurgeAsync(default);
        await store.PurgeAsync(default); // Double purge should not error.

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

    // Go: TestFileStorePurge server/filestore_test.go:709
    [Fact]
    public async Task Purge_empty_store_is_safe()
    {
        await using var store = CreateStore("purge-empty");

        await store.PurgeAsync(default);

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

    // Go: TestFileStorePurge server/filestore_test.go:709
    [Fact]
    public async Task Purge_with_prior_removes()
    {
        await using var store = CreateStore("purge-prior-rm");

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

        // Remove some messages first.
        await store.RemoveAsync(2, default);
        await store.RemoveAsync(4, default);
        await store.RemoveAsync(6, default);

        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)7);

        await store.PurgeAsync(default);

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

    // Go: TestFileStorePurge server/filestore_test.go:776
    [Fact]
    public async Task Purge_then_store_then_purge_again()
    {
        await using var store = CreateStore("purge-cycle");

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

        await store.PurgeAsync(default);

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

        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)3);

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

    // Go: TestFileStorePurge server/filestore_test.go:709
    [Fact]
    public async Task Purge_data_file_is_deleted()
    {
        var subDir = "purge-file";
        var dir = Path.Combine(_dir, subDir);

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

            await store.PurgeAsync(default);
        }

        // The data file should be cleaned up or empty after purge.
        var dataFile = Path.Combine(dir, "messages.jsonl");
        if (File.Exists(dataFile))
        {
            var content = File.ReadAllText(dataFile);
            content.Trim().ShouldBeEmpty();
        }
    }
}
