// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStoreNoFSSWhenNoSubjects,
//                    TestFileStoreNoFSSBugAfterRemoveFirst,
//                    TestFileStoreNoFSSAfterRecover,
//                    TestFileStoreSubjectStateCacheExpiration,
//                    TestFileStoreSubjectsTotals,
//                    TestFileStoreSubjectCorruption,
//                    TestFileStoreFilteredPendingBug,
//                    TestFileStoreFilteredFirstMatchingBug,
//                    TestFileStoreExpireSubjectMeta,
//                    TestFileStoreAllFilteredStateWithDeleted

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

namespace NATS.Server.Tests.JetStream.Storage;

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

    public FileStoreSubjectTests()
    {
        _dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-subject-{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: TestFileStoreNoFSSWhenNoSubjects server/filestore_test.go:4251
    [Fact]
    public async Task Store_with_empty_subject()
    {
        await using var store = CreateStore("empty-subj");

        // Store messages with empty subject (like raft state).
        for (var i = 0; i < 10; i++)
            await store.AppendAsync(string.Empty, "raft state"u8.ToArray(), default);

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

        // Should be loadable.
        var msg = await store.LoadAsync(1, default);
        msg.ShouldNotBeNull();
        msg!.Subject.ShouldBe(string.Empty);
    }

    // Go: TestFileStoreNoFSSBugAfterRemoveFirst server/filestore_test.go:4289
    [Fact]
    public async Task Remove_first_with_different_subjects()
    {
        await using var store = CreateStore("rm-first-subj");

        await store.AppendAsync("foo", "first"u8.ToArray(), default);
        await store.AppendAsync("bar", "second"u8.ToArray(), default);
        await store.AppendAsync("foo", "third"u8.ToArray(), default);

        // Remove first message.
        (await store.RemoveAsync(1, default)).ShouldBeTrue();

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

        // LoadLastBySubject should still work for "foo".
        var lastFoo = await store.LoadLastBySubjectAsync("foo", default);
        lastFoo.ShouldNotBeNull();
        lastFoo!.Sequence.ShouldBe((ulong)3);
    }

    // Go: TestFileStoreNoFSSAfterRecover server/filestore_test.go:4333
    [Fact]
    public async Task Subject_filtering_after_recovery()
    {
        var subDir = "subj-after-recover";

        await using (var store = CreateStore(subDir))
        {
            await store.AppendAsync("foo.1", "a"u8.ToArray(), default);
            await store.AppendAsync("foo.2", "b"u8.ToArray(), default);
            await store.AppendAsync("bar.1", "c"u8.ToArray(), default);
            await store.AppendAsync("foo.1", "d"u8.ToArray(), default);
        }

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

            // LoadLastBySubject should work after recovery.
            var lastFoo1 = await store.LoadLastBySubjectAsync("foo.1", default);
            lastFoo1.ShouldNotBeNull();
            lastFoo1!.Sequence.ShouldBe((ulong)4);
            lastFoo1.Payload.ToArray().ShouldBe("d"u8.ToArray());

            var lastBar1 = await store.LoadLastBySubjectAsync("bar.1", default);
            lastBar1.ShouldNotBeNull();
            lastBar1!.Sequence.ShouldBe((ulong)3);
        }
    }

    // Go: TestFileStoreSubjectStateCacheExpiration server/filestore_test.go:4143
    [Fact(Skip = "SubjectsState not yet implemented in .NET FileStore")]
    public async Task Subject_state_cache_expiration()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreSubjectsTotals server/filestore_test.go:4948
    [Fact(Skip = "SubjectsTotals not yet implemented in .NET FileStore")]
    public async Task Subjects_totals_with_wildcards()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreSubjectCorruption server/filestore_test.go:6466
    [Fact(Skip = "SubjectForSeq not yet implemented in .NET FileStore")]
    public async Task Subject_corruption_detection()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreFilteredPendingBug server/filestore_test.go:3414
    [Fact(Skip = "FilteredState not yet implemented in .NET FileStore")]
    public async Task Filtered_pending_no_match_returns_zero()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreFilteredFirstMatchingBug server/filestore_test.go:4448
    [Fact(Skip = "LoadNextMsg not yet implemented in .NET FileStore")]
    public async Task Filtered_first_matching_finds_correct_sequence()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreExpireSubjectMeta server/filestore_test.go:4014
    [Fact(Skip = "SubjectsState not yet implemented in .NET FileStore")]
    public async Task Expired_subject_metadata_cleans_up()
    {
        await Task.CompletedTask;
    }

    // Go: TestFileStoreAllFilteredStateWithDeleted server/filestore_test.go:4827
    [Fact(Skip = "FilteredState not yet implemented in .NET FileStore")]
    public async Task Filtered_state_with_deleted_messages()
    {
        await Task.CompletedTask;
    }

    // Test LoadLastBySubject with multiple subjects and removes.
    [Fact]
    public async Task LoadLastBySubject_after_removes()
    {
        await using var store = CreateStore("last-after-rm");

        await store.AppendAsync("foo", "a"u8.ToArray(), default);
        await store.AppendAsync("foo", "b"u8.ToArray(), default);
        await store.AppendAsync("foo", "c"u8.ToArray(), default);

        // Remove the last message on "foo" (seq 3).
        await store.RemoveAsync(3, default);

        var last = await store.LoadLastBySubjectAsync("foo", default);
        last.ShouldNotBeNull();
        last!.Sequence.ShouldBe((ulong)2);
        last.Payload.ToArray().ShouldBe("b"u8.ToArray());
    }

    // Test LoadLastBySubject when all messages on that subject are removed.
    [Fact]
    public async Task LoadLastBySubject_all_removed_returns_null()
    {
        await using var store = CreateStore("last-all-rm");

        await store.AppendAsync("foo", "a"u8.ToArray(), default);
        await store.AppendAsync("foo", "b"u8.ToArray(), default);
        await store.AppendAsync("bar", "c"u8.ToArray(), default);

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

        var last = await store.LoadLastBySubjectAsync("foo", default);
        last.ShouldBeNull();

        // "bar" should still be present.
        var lastBar = await store.LoadLastBySubjectAsync("bar", default);
        lastBar.ShouldNotBeNull();
        lastBar!.Sequence.ShouldBe((ulong)3);
    }

    // Test multiple subjects interleaved.
    [Fact]
    public async Task Multiple_subjects_interleaved()
    {
        await using var store = CreateStore("interleaved");

        for (var i = 0; i < 20; i++)
        {
            var subject = i % 3 == 0 ? "alpha" : (i % 3 == 1 ? "beta" : "gamma");
            await store.AppendAsync(subject, Encoding.UTF8.GetBytes($"msg-{i}"), default);
        }

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

        // Verify all subjects are loadable and correct.
        for (ulong i = 1; i <= 20; i++)
        {
            var msg = await store.LoadAsync(i, default);
            msg.ShouldNotBeNull();
            var idx = (int)(i - 1);
            var expectedSubj = idx % 3 == 0 ? "alpha" : (idx % 3 == 1 ? "beta" : "gamma");
            msg!.Subject.ShouldBe(expectedSubj);
        }
    }

    // Test LoadLastBySubject with case-sensitive subjects.
    [Fact]
    public async Task LoadLastBySubject_is_case_sensitive()
    {
        await using var store = CreateStore("case-sensitive");

        await store.AppendAsync("Foo", "upper"u8.ToArray(), default);
        await store.AppendAsync("foo", "lower"u8.ToArray(), default);

        var lastUpper = await store.LoadLastBySubjectAsync("Foo", default);
        lastUpper.ShouldNotBeNull();
        lastUpper!.Payload.ToArray().ShouldBe("upper"u8.ToArray());

        var lastLower = await store.LoadLastBySubjectAsync("foo", default);
        lastLower.ShouldNotBeNull();
        lastLower!.Payload.ToArray().ShouldBe("lower"u8.ToArray());
    }

    // Test subject preservation across restarts.
    [Fact]
    public async Task Subject_preserved_across_restart()
    {
        var subDir = "subj-restart";

        await using (var store = CreateStore(subDir))
        {
            await store.AppendAsync("topic.a", "one"u8.ToArray(), default);
            await store.AppendAsync("topic.b", "two"u8.ToArray(), default);
            await store.AppendAsync("topic.c", "three"u8.ToArray(), default);
        }

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

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

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

    // Go: TestFileStoreNumPendingLastBySubject server/filestore_test.go:6501
    [Fact(Skip = "NumPending not yet implemented in .NET FileStore")]
    public async Task NumPending_last_per_subject()
    {
        await Task.CompletedTask;
    }

    // Test many distinct subjects.
    [Fact]
    public async Task Many_distinct_subjects()
    {
        await using var store = CreateStore("many-subjects");

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

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

        // Each subject should have exactly one message.
        for (var i = 0; i < 100; i++)
        {
            var last = await store.LoadLastBySubjectAsync($"kv.{i}", default);
            last.ShouldNotBeNull();
            last!.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes($"value-{i}"));
        }
    }
}
