// Reference: golang/nats-server/server/filestore_test.go
// Tests ported from: TestFileStorePurgeEx, TestFileStorePurgeExWithSubject,
//   TestFileStorePurgeExKeepOneBug, TestFileStoreCompact, TestFileStoreStreamTruncate,
//   TestFileStoreState, TestFileStoreFilteredState, TestFileStoreSubjectsState,
//   TestFileStoreGetSeqFromTime

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

namespace NATS.Server.Tests.JetStream.Storage;

/// <summary>
/// Tests for FileStore tombstone tracking and purge operations:
/// PurgeEx, Compact, Truncate, FilteredState, SubjectsState, SubjectsTotals,
/// State (with deleted sequences), and GetSeqFromTime.
/// Reference: golang/nats-server/server/filestore_test.go
/// </summary>
public sealed class FileStorePurgeBlockTests : IDisposable
{
    private readonly string _dir;

    public FileStorePurgeBlockTests()
    {
        _dir = Path.Combine(Path.GetTempPath(), $"nats-js-purgeblock-{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);
    }

    // -------------------------------------------------------------------------
    // PurgeEx tests
    // -------------------------------------------------------------------------

    // Go: TestFileStorePurgeExWithSubject — filestore_test.go:~867
    [Fact]
    public async Task PurgeEx_BySubject_RemovesMatchingMessages()
    {
        await using var store = CreateStore("purgex-subject");

        // Store 5 messages on "foo" and 5 on "bar"
        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 stateBeforePurge = await store.GetStateAsync(default);
        stateBeforePurge.Messages.ShouldBe(10UL);

        // Purge all "foo" messages (seq=0 means no upper limit; keep=0 means keep none)
        var purged = store.PurgeEx("foo", 0, 0);
        purged.ShouldBe(5UL);

        // Only "bar" messages remain
        var state = await store.GetStateAsync(default);
        state.Messages.ShouldBe(5UL);

        var remaining = await store.ListAsync(default);
        remaining.All(m => m.Subject == "bar").ShouldBeTrue();
    }

    // Go: TestFileStorePurgeExKeepOneBug — filestore_test.go:~910
    [Fact]
    public async Task PurgeEx_WithKeep_RetainsNewestMessages()
    {
        await using var store = CreateStore("purgex-keep");

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

        // Purge keeping the 3 newest
        var purged = store.PurgeEx("events", 0, 3);
        purged.ShouldBe(7UL);

        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(3);

        // The retained messages should be the 3 highest sequences (8, 9, 10)
        var seqs = remaining.Select(m => m.Sequence).OrderBy(s => s).ToArray();
        seqs[0].ShouldBe(8UL);
        seqs[1].ShouldBe(9UL);
        seqs[2].ShouldBe(10UL);
    }

    // Go: TestFileStorePurgeEx — filestore_test.go:~855
    [Fact]
    public async Task PurgeEx_WithSeqLimit_OnlyPurgesBelowSequence()
    {
        await using var store = CreateStore("purgex-seqlimit");

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

        // Purge "data" messages with seq <= 5 (keep=0)
        var purged = store.PurgeEx("data", 5, 0);
        purged.ShouldBe(5UL);

        // Messages 6-10 should remain
        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(5);
        remaining.Min(m => m.Sequence).ShouldBe(6UL);
        remaining.Max(m => m.Sequence).ShouldBe(10UL);
    }

    // Go: PurgeEx with wildcard subject — filestore_test.go:~867
    [Fact]
    public async Task PurgeEx_WithWildcardSubject_RemovesAllMatchingSubjects()
    {
        await using var store = CreateStore("purgex-wildcard");

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

        var purged = store.PurgeEx("foo.*", 0, 0);
        purged.ShouldBe(3UL);

        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(1);
        remaining[0].Subject.ShouldBe("bar.a");
    }

    // Go: PurgeEx with > wildcard — filestore_test.go:~867
    [Fact]
    public async Task PurgeEx_WithGtWildcard_RemovesAllMatchingSubjects()
    {
        await using var store = CreateStore("purgex-gt-wildcard");

        await store.AppendAsync("a.b.c", "m1"u8.ToArray(), default);
        await store.AppendAsync("a.b.d", "m2"u8.ToArray(), default);
        await store.AppendAsync("a.x", "m3"u8.ToArray(), default);
        await store.AppendAsync("b.x", "m4"u8.ToArray(), default);

        var purged = store.PurgeEx("a.>", 0, 0);
        purged.ShouldBe(3UL);

        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(1);
        remaining[0].Subject.ShouldBe("b.x");
    }

    // -------------------------------------------------------------------------
    // Compact tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreCompact — filestore_test.go:~964
    [Fact]
    public async Task Compact_RemovesMessagesBeforeSequence()
    {
        await using var store = CreateStore("compact-basic");

        // Store 10 messages
        for (var i = 1; i <= 10; i++)
            await store.AppendAsync("test", Encoding.UTF8.GetBytes($"msg{i}"), default);

        // Compact to remove messages with seq < 5 (removes 1, 2, 3, 4)
        var removed = store.Compact(5);
        removed.ShouldBe(4UL);

        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(6); // 5-10

        remaining.Min(m => m.Sequence).ShouldBe(5UL);
        remaining.Max(m => m.Sequence).ShouldBe(10UL);

        // Sequence 1-4 should no longer be loadable
        (await store.LoadAsync(1, default)).ShouldBeNull();
        (await store.LoadAsync(4, default)).ShouldBeNull();

        // Sequence 5 should still exist
        (await store.LoadAsync(5, default)).ShouldNotBeNull();
    }

    // -------------------------------------------------------------------------
    // Truncate tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreStreamTruncate — filestore_test.go:~1035
    [Fact]
    public async Task Truncate_RemovesMessagesAfterSequence()
    {
        await using var store = CreateStore("truncate-basic");

        // Store 10 messages
        for (var i = 1; i <= 10; i++)
            await store.AppendAsync("stream", Encoding.UTF8.GetBytes($"m{i}"), default);

        // Truncate at seq=5 (removes 6, 7, 8, 9, 10)
        store.Truncate(5);

        var remaining = await store.ListAsync(default);
        remaining.Count.ShouldBe(5); // 1-5

        remaining.Min(m => m.Sequence).ShouldBe(1UL);
        remaining.Max(m => m.Sequence).ShouldBe(5UL);

        // Messages 6-10 should be gone
        (await store.LoadAsync(6, default)).ShouldBeNull();
        (await store.LoadAsync(10, default)).ShouldBeNull();

        // Message 5 should still exist
        (await store.LoadAsync(5, default)).ShouldNotBeNull();
    }

    // -------------------------------------------------------------------------
    // FilteredState tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreFilteredState — filestore_test.go:~1200
    [Fact]
    public async Task FilteredState_ReturnsCorrectState()
    {
        await using var store = CreateStore("filteredstate");

        // Store 5 messages on "orders" and 5 on "invoices"
        for (var i = 1; i <= 5; i++)
            await store.AppendAsync("orders", Encoding.UTF8.GetBytes($"o{i}"), default);
        for (var i = 1; i <= 5; i++)
            await store.AppendAsync("invoices", Encoding.UTF8.GetBytes($"inv{i}"), default);

        // FilteredState for "orders" from seq=1
        var ordersState = store.FilteredState(1, "orders");
        ordersState.Msgs.ShouldBe(5UL);
        ordersState.First.ShouldBe(1UL);
        ordersState.Last.ShouldBe(5UL);

        // FilteredState for "invoices" from seq=1
        var invoicesState = store.FilteredState(1, "invoices");
        invoicesState.Msgs.ShouldBe(5UL);
        invoicesState.First.ShouldBe(6UL);
        invoicesState.Last.ShouldBe(10UL);

        // FilteredState from seq=7 (only 4 invoices remain)
        var lateInvoices = store.FilteredState(7, "invoices");
        lateInvoices.Msgs.ShouldBe(4UL);
        lateInvoices.First.ShouldBe(7UL);
        lateInvoices.Last.ShouldBe(10UL);

        // No match for non-existent subject
        var noneState = store.FilteredState(1, "orders.unknown");
        noneState.Msgs.ShouldBe(0UL);
    }

    // -------------------------------------------------------------------------
    // SubjectsState tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreSubjectsState — filestore_test.go:~1266
    [Fact]
    public async Task SubjectsState_ReturnsPerSubjectState()
    {
        await using var store = CreateStore("subjectsstate");

        await store.AppendAsync("a.1", "msg"u8.ToArray(), default);
        await store.AppendAsync("a.2", "msg"u8.ToArray(), default);
        await store.AppendAsync("a.1", "msg"u8.ToArray(), default);
        await store.AppendAsync("b.1", "msg"u8.ToArray(), default);

        var state = store.SubjectsState("a.>");

        state.ShouldContainKey("a.1");
        state.ShouldContainKey("a.2");
        state.ShouldNotContainKey("b.1");

        state["a.1"].Msgs.ShouldBe(2UL);
        state["a.1"].First.ShouldBe(1UL);
        state["a.1"].Last.ShouldBe(3UL);

        state["a.2"].Msgs.ShouldBe(1UL);
        state["a.2"].First.ShouldBe(2UL);
        state["a.2"].Last.ShouldBe(2UL);
    }

    // -------------------------------------------------------------------------
    // SubjectsTotals tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreSubjectsTotals — filestore_test.go:~1300
    [Fact]
    public async Task SubjectsTotals_ReturnsPerSubjectCounts()
    {
        await using var store = CreateStore("subjectstotals");

        await store.AppendAsync("x.1", "m"u8.ToArray(), default);
        await store.AppendAsync("x.1", "m"u8.ToArray(), default);
        await store.AppendAsync("x.2", "m"u8.ToArray(), default);
        await store.AppendAsync("y.1", "m"u8.ToArray(), default);
        await store.AppendAsync("x.3", "m"u8.ToArray(), default);

        var totals = store.SubjectsTotals("x.*");

        totals.ShouldContainKey("x.1");
        totals.ShouldContainKey("x.2");
        totals.ShouldContainKey("x.3");
        totals.ShouldNotContainKey("y.1");

        totals["x.1"].ShouldBe(2UL);
        totals["x.2"].ShouldBe(1UL);
        totals["x.3"].ShouldBe(1UL);
    }

    // -------------------------------------------------------------------------
    // State (with deleted sequences) tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreState — filestore_test.go:~420
    [Fact]
    public async Task State_IncludesDeletedSequences()
    {
        await using var store = CreateStore("state-deleted");

        // Store 10 messages
        for (var i = 1; i <= 10; i++)
            await store.AppendAsync("events", Encoding.UTF8.GetBytes($"e{i}"), default);

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

        var state = store.State();

        state.Msgs.ShouldBe(7UL);
        state.FirstSeq.ShouldBe(1UL);
        state.LastSeq.ShouldBe(10UL);
        state.NumDeleted.ShouldBe(3);

        state.Deleted.ShouldNotBeNull();
        state.Deleted!.ShouldContain(3UL);
        state.Deleted.ShouldContain(5UL);
        state.Deleted.ShouldContain(7UL);
        state.Deleted.Length.ShouldBe(3);

        // NumSubjects: all messages are on "events"
        state.NumSubjects.ShouldBe(1);
        state.Subjects.ShouldNotBeNull();
        state.Subjects!["events"].ShouldBe(7UL);
    }

    // -------------------------------------------------------------------------
    // GetSeqFromTime tests
    // -------------------------------------------------------------------------

    // Go: TestFileStoreGetSeqFromTime — filestore_test.go:~1570
    [Fact]
    public async Task GetSeqFromTime_ReturnsCorrectSequence()
    {
        await using var store = CreateStore("getseqfromtime");

        // Store 5 messages; we'll query by the timestamp of the 3rd message
        var timestamps = new List<DateTime>();
        for (var i = 1; i <= 5; i++)
        {
            await store.AppendAsync("time.test", Encoding.UTF8.GetBytes($"t{i}"), default);
            var msgs = await store.ListAsync(default);
            timestamps.Add(msgs[^1].TimestampUtc);
            // Small delay to ensure distinct timestamps
            await Task.Delay(5);
        }

        // Query for first seq at or after the timestamp of msg 3
        var targetTime = timestamps[2]; // timestamp of sequence 3
        var seq = store.GetSeqFromTime(targetTime);
        seq.ShouldBe(3UL);

        // Query with a time before all messages: should return 1
        var beforeAll = timestamps[0].AddMilliseconds(-100);
        store.GetSeqFromTime(beforeAll).ShouldBe(1UL);

        // Query with a time after all messages: should return last+1
        var afterAll = timestamps[^1].AddSeconds(1);
        store.GetSeqFromTime(afterAll).ShouldBe(6UL); // _last + 1
    }

    // -------------------------------------------------------------------------
    // MsgBlock enhancements
    // -------------------------------------------------------------------------

    // Go: filestore.go dmap — soft-delete tracking and enumeration
    [Fact]
    public async Task MsgBlock_IsDeleted_AndEnumerateNonDeleted_Work()
    {
        await using var store = CreateStore("block-enumerate");

        // Store 5 messages on 2 subjects
        await store.AppendAsync("a.1", "m1"u8.ToArray(), default);
        await store.AppendAsync("a.2", "m2"u8.ToArray(), default);
        await store.AppendAsync("a.1", "m3"u8.ToArray(), default);
        await store.AppendAsync("b.1", "m4"u8.ToArray(), default);
        await store.AppendAsync("a.2", "m5"u8.ToArray(), default);

        // Delete sequences 2 and 4
        await store.RemoveAsync(2, default);
        await store.RemoveAsync(4, default);

        // Verify the state after deletion
        var all = await store.ListAsync(default);
        all.Count.ShouldBe(3);
        all.Select(m => m.Sequence).ShouldBe([1UL, 3UL, 5UL]);

        // FilteredState should only see non-deleted
        var aState = store.FilteredState(1, "a.1");
        aState.Msgs.ShouldBe(2UL); // sequences 1 and 3
    }
}
