// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStoreMsgLimit, TestFileStoreMsgLimitBug,
//                    TestFileStoreBytesLimit, TestFileStoreBytesLimitWithDiscardNew,
//                    TestFileStoreAgeLimit, TestFileStoreMaxMsgsPerSubject,
//                    TestFileStoreMaxMsgsAndMaxMsgsPerSubject,
//                    TestFileStoreUpdateMaxMsgsPerSubject

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

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

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

    public FileStoreLimitsTests()
    {
        _dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-limits-{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: TestFileStoreMsgLimit server/filestore_test.go:484
    [Fact]
    public async Task TrimToMaxMessages_maintains_limit()
    {
        await using var store = CreateStore("msg-limit");

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

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

        // Store one more, then trim.
        await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
        store.TrimToMaxMessages(10);

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

        // Seq 1 should be evicted.
        (await store.LoadAsync(1, default)).ShouldBeNull();
    }

    // Go: TestFileStoreMsgLimitBug server/filestore_test.go:518
    [Fact]
    public async Task TrimToMaxMessages_one_across_restart()
    {
        var subDir = "msg-limit-bug";

        await using (var store = CreateStore(subDir))
        {
            await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
            await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
            store.TrimToMaxMessages(1);
        }

        // Reopen and store one more.
        await using (var store = CreateStore(subDir))
        {
            var state = await store.GetStateAsync(default);
            state.Messages.ShouldBe((ulong)1);

            await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
            store.TrimToMaxMessages(1);

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

    // Go: TestFileStoreMsgLimit server/filestore_test.go:484
    [Fact]
    public async Task TrimToMaxMessages_repeated_trims()
    {
        await using var store = CreateStore("repeated-trim");

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

        store.TrimToMaxMessages(10);
        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)10);
        (await store.GetStateAsync(default)).FirstSeq.ShouldBe((ulong)11);

        store.TrimToMaxMessages(5);
        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)5);
        (await store.GetStateAsync(default)).FirstSeq.ShouldBe((ulong)16);

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

    // Go: TestFileStoreBytesLimit server/filestore_test.go:537
    [Fact]
    public async Task Bytes_accumulate_correctly()
    {
        await using var store = CreateStore("bytes-accum");

        var payload = new byte[512];
        const int count = 10;

        for (var i = 0; i < count; i++)
            await store.AppendAsync("foo", payload, default);

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

    // Go: TestFileStoreBytesLimit server/filestore_test.go:537
    [Fact]
    public async Task TrimToMaxMessages_reduces_bytes()
    {
        await using var store = CreateStore("bytes-trim");

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

        var beforeState = await store.GetStateAsync(default);
        beforeState.Bytes.ShouldBe((ulong)1000);

        store.TrimToMaxMessages(5);

        var afterState = await store.GetStateAsync(default);
        afterState.Messages.ShouldBe((ulong)5);
        afterState.Bytes.ShouldBe((ulong)500);
    }

    // Go: TestFileStoreAgeLimit server/filestore_test.go:616
    [SlopwatchSuppress("SW004", "MaxAge TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing time-based expiration")]
    [Fact]
    public async Task MaxAge_expires_old_messages()
    {
        // MaxAgeMs = 200ms
        await using var store = CreateStore("age-limit", new FileStoreOptions { MaxAgeMs = 200 });

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

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

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

        // Trigger pruning by appending a new message.
        await store.AppendAsync("foo", "trigger"u8.ToArray(), default);

        var state = await store.GetStateAsync(default);
        // Only the freshly-appended trigger message should remain.
        state.Messages.ShouldBe((ulong)1);
    }

    // Go: TestFileStoreAgeLimit server/filestore_test.go:660
    [SlopwatchSuppress("SW004", "MaxAge TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing time-based expiration")]
    [Fact]
    public async Task MaxAge_timer_fires_again_for_second_batch()
    {
        await using var store = CreateStore("age-second-batch", new FileStoreOptions { MaxAgeMs = 200 });

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

        await Task.Delay(300);

        // Trigger pruning.
        await store.AppendAsync("foo", "trigger1"u8.ToArray(), default);
        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)1);

        // Second batch.
        for (var i = 0; i < 3; i++)
            await store.AppendAsync("foo", "batch2"u8.ToArray(), default);

        await Task.Delay(300);

        await store.AppendAsync("foo", "trigger2"u8.ToArray(), default);
        (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)1);
    }

    // Go: TestFileStoreAgeLimit server/filestore_test.go:616
    [SlopwatchSuppress("SW004", "MaxAge TTL expiry test requires real wall-clock time to elapse; verifying zero-age means no expiration needs a delay window")]
    [Fact]
    public async Task MaxAge_zero_means_no_expiration()
    {
        await using var store = CreateStore("age-zero", new FileStoreOptions { MaxAgeMs = 0 });

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

        await Task.Delay(100);

        // Trigger append to check pruning.
        await store.AppendAsync("foo", "trigger"u8.ToArray(), default);

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

    // Go: TestFileStoreMsgLimit server/filestore_test.go:484
    [Fact]
    public async Task TrimToMaxMessages_zero_removes_all()
    {
        await using var store = CreateStore("trim-zero");

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

        store.TrimToMaxMessages(0);

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

    // Go: TestFileStoreMsgLimit server/filestore_test.go:484
    [Fact]
    public async Task TrimToMaxMessages_larger_than_count_is_noop()
    {
        await using var store = CreateStore("trim-noop");

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

        store.TrimToMaxMessages(100);

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

    // Go: TestFileStoreBytesLimit server/filestore_test.go:537
    [Fact]
    public async Task Bytes_decrease_after_remove()
    {
        await using var store = CreateStore("bytes-rm");

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

        var before = await store.GetStateAsync(default);
        before.Bytes.ShouldBe((ulong)500);

        await store.RemoveAsync(1, default);
        await store.RemoveAsync(3, default);

        var after = await store.GetStateAsync(default);
        after.Bytes.ShouldBe((ulong)300);
    }

    // Go: TestFileStoreBytesLimitWithDiscardNew server/filestore_test.go:583
    [Fact]
    public async Task Bytes_limit_with_discard_new_rejects_over_limit()
    {
        var payload = new byte[7];
        await using var store = CreateStore("bytes-discard-new", new FileStoreOptions
        {
            MaxBytes = 20,
            Discard = DiscardPolicy.New,
        });

        // 2 messages fit (14 bytes <= 20)
        await store.AppendAsync("tiny", payload, default);
        await store.AppendAsync("tiny", payload, default);

        // 3rd rejected (14 + 7 = 21 > 20)
        await Should.ThrowAsync<StoreCapacityException>(
            async () => await store.AppendAsync("tiny", payload, default));

        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe(2UL);
        state.Bytes.ShouldBe(14UL);
    }

    // Go: TestFileStoreMaxMsgsPerSubject server/filestore_test.go:4065
    [Fact]
    public async Task MaxMsgsPerSubject_enforces_per_subject_limit()
    {
        await using var store = CreateStore("max-per-subj", new FileStoreOptions { MaxMsgsPerSubject = 2 });

        // Store 5 messages on "foo" — only last 2 should survive.
        for (var i = 0; i < 5; i++)
            await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"foo-{i}"), default);

        // Store 3 messages on "bar" — only last 2 should survive.
        for (var i = 0; i < 3; i++)
            await store.AppendAsync("bar", Encoding.UTF8.GetBytes($"bar-{i}"), default);

        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe((ulong)4); // 2 foo + 2 bar

        // Verify oldest foo messages were evicted.
        (await store.LoadAsync(1, default)).ShouldBeNull();
        (await store.LoadAsync(2, default)).ShouldBeNull();
        (await store.LoadAsync(3, default)).ShouldBeNull();

        // Last 2 foo messages should survive (seqs 4 and 5).
        (await store.LoadAsync(4, default)).ShouldNotBeNull();
        (await store.LoadAsync(5, default)).ShouldNotBeNull();
    }

    // Go: TestFileStoreMaxMsgsAndMaxMsgsPerSubject server/filestore_test.go:4098
    [Fact]
    public async Task MaxMsgs_and_MaxMsgsPerSubject_combined()
    {
        await using var store = CreateStore("max-combined", new FileStoreOptions { MaxMsgsPerSubject = 3 });

        // Store messages across multiple subjects.
        for (var i = 0; i < 5; i++)
            await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"foo-{i}"), default);
        for (var i = 0; i < 5; i++)
            await store.AppendAsync("bar", Encoding.UTF8.GetBytes($"bar-{i}"), default);

        var state = await store.GetStateAsync(default);
        // Each subject limited to 3 → 6 total.
        state.Messages.ShouldBe((ulong)6);

        // Verify per-subject: last 3 of each subject survive.
        var fooLast = await store.LoadLastBySubjectAsync("foo", default);
        fooLast.ShouldNotBeNull();
        fooLast!.Sequence.ShouldBe((ulong)5);

        var barLast = await store.LoadLastBySubjectAsync("bar", default);
        barLast.ShouldNotBeNull();
        barLast!.Sequence.ShouldBe((ulong)10);
    }

    // Go: TestFileStoreUpdateMaxMsgsPerSubject server/filestore_test.go:4563
    [Fact]
    public async Task UpdateConfig_changes_MaxMsgsPerSubject()
    {
        await using var store = CreateStore("update-max-per-subj");

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

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

        // Update config to limit to 3 per subject.
        store.UpdateConfig(new StreamConfig { MaxMsgsPer = 3 });

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

        // Only the last 3 messages should remain.
        (await store.LoadAsync(8, default)).ShouldNotBeNull();
        (await store.LoadAsync(9, default)).ShouldNotBeNull();
        (await store.LoadAsync(10, default)).ShouldNotBeNull();
        (await store.LoadAsync(7, default)).ShouldBeNull();
    }

    // Go: TestFileStoreMsgLimit server/filestore_test.go:484
    [Fact]
    public async Task TrimToMaxMessages_persists_across_restart()
    {
        var subDir = "trim-persist";

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

            store.TrimToMaxMessages(5);
        }

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

    // Go: TestFileStoreAgeLimit server/filestore_test.go:616
    [SlopwatchSuppress("SW004", "MaxAge TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing time-based expiration")]
    [Fact]
    public async Task MaxAge_with_interior_deletes()
    {
        await using var store = CreateStore("age-interior", new FileStoreOptions { MaxAgeMs = 200 });

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

        // Remove some interior messages.
        await store.RemoveAsync(3, default);
        await store.RemoveAsync(5, default);
        await store.RemoveAsync(7, default);

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

        await Task.Delay(300);

        // Trigger pruning.
        await store.AppendAsync("foo", "trigger"u8.ToArray(), default);

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

    // Go: TestFileStoreMsgLimit server/filestore_test.go:484
    [Fact]
    public async Task Sequence_numbers_monotonically_increase_through_trimming()
    {
        await using var store = CreateStore("seq-mono");

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

        store.TrimToMaxMessages(5);

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

        // Append more.
        var nextSeq = await store.AppendAsync("foo", "after-trim"u8.ToArray(), default);
        nextSeq.ShouldBe((ulong)16);

        state = await store.GetStateAsync(default);
        state.LastSeq.ShouldBe((ulong)16);
        state.Messages.ShouldBe((ulong)6);
    }
}
