// Reference: golang/nats-server/server/avl/seqset_test.go
// Tests ported / inspired by:
//   TestSequenceSetBasic    → Add_Contains_Count_BasicOperations
//   TestSequenceSetRange    → GetEnumerator_ReturnsAscendingOrder
//   TestSequenceSetDelete   → Remove_SplitsAndTrimsRanges
//   (range compression)     → Add_ContiguousSequences_CompressesToOneRange
//   (binary search)         → Contains_BinarySearchCorrectness
//   (boundary)              → Add_Remove_AtBoundaries

using NATS.Server.JetStream.Storage;

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

/// <summary>
/// Unit tests for <see cref="SequenceSet"/> — the range-compressed sorted set
/// used to track soft-deleted sequences in JetStream FileStore blocks.
///
/// Reference: golang/nats-server/server/avl/seqset_test.go
/// </summary>
public sealed class SequenceSetTests
{
    // -------------------------------------------------------------------------
    // Basic Add / Contains / Count
    // -------------------------------------------------------------------------

    // Go: TestSequenceSetBasic — empty set has zero count
    [Fact]
    public void Count_EmptySet_ReturnsZero()
    {
        var ss = new SequenceSet();
        ss.Count.ShouldBe(0);
        ss.IsEmpty.ShouldBeTrue();
    }

    // Go: TestSequenceSetBasic — single element is found
    [Fact]
    public void Add_SingleSequence_ContainsIt()
    {
        var ss = new SequenceSet();
        ss.Add(42).ShouldBeTrue();
        ss.Contains(42).ShouldBeTrue();
        ss.Count.ShouldBe(1);
        ss.IsEmpty.ShouldBeFalse();
    }

    // Go: duplicate insert returns false (already present)
    [Fact]
    public void Add_DuplicateSequence_ReturnsFalse()
    {
        var ss = new SequenceSet();
        ss.Add(10).ShouldBeTrue();
        ss.Add(10).ShouldBeFalse();
        ss.Count.ShouldBe(1);
    }

    // Go: non-member returns false on Contains
    [Fact]
    public void Contains_NonMember_ReturnsFalse()
    {
        var ss = new SequenceSet();
        ss.Add(5);
        ss.Contains(4).ShouldBeFalse();
        ss.Contains(6).ShouldBeFalse();
        ss.Contains(0).ShouldBeFalse();
        ss.Contains(ulong.MaxValue).ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Range compression
    // -------------------------------------------------------------------------

    // Adding three contiguous sequences should compress to a single range.
    // This is the key efficiency property of SequenceSet vs HashSet.
    [Fact]
    public void Add_ContiguousSequences_CompressesToOneRange()
    {
        var ss = new SequenceSet();
        ss.Add(1);
        ss.Add(2);
        ss.Add(3);

        ss.Count.ShouldBe(3);
        ss.RangeCount.ShouldBe(1); // single range [1, 3]
        ss.Contains(1).ShouldBeTrue();
        ss.Contains(2).ShouldBeTrue();
        ss.Contains(3).ShouldBeTrue();
    }

    // Adding in reverse order should still compress.
    [Fact]
    public void Add_ContiguousReverse_CompressesToOneRange()
    {
        var ss = new SequenceSet();
        ss.Add(3);
        ss.Add(2);
        ss.Add(1);

        ss.Count.ShouldBe(3);
        ss.RangeCount.ShouldBe(1); // single range [1, 3]
    }

    // Two separate gaps should stay as two ranges.
    [Fact]
    public void Add_WithGap_TwoRanges()
    {
        var ss = new SequenceSet();
        ss.Add(1);
        ss.Add(2);
        ss.Add(4); // gap at 3
        ss.Add(5);

        ss.Count.ShouldBe(4);
        ss.RangeCount.ShouldBe(2); // [1,2] and [4,5]
    }

    // Filling the gap merges to one range.
    [Fact]
    public void Add_FillsGap_MergesToOneRange()
    {
        var ss = new SequenceSet();
        ss.Add(1);
        ss.Add(2);
        ss.Add(4);
        ss.Add(5);
        ss.RangeCount.ShouldBe(2);

        // Fill the gap.
        ss.Add(3);
        ss.RangeCount.ShouldBe(1); // [1, 5]
        ss.Count.ShouldBe(5);
    }

    // Large run of contiguous sequences stays as one range.
    [Fact]
    public void Add_LargeContiguousRun_OnlyOneRange()
    {
        var ss = new SequenceSet();
        for (ulong i = 1; i <= 10_000; i++)
            ss.Add(i);

        ss.Count.ShouldBe(10_000);
        ss.RangeCount.ShouldBe(1);
    }

    // -------------------------------------------------------------------------
    // Remove / split / trim
    // -------------------------------------------------------------------------

    // Removing from an empty set returns false.
    [Fact]
    public void Remove_EmptySet_ReturnsFalse()
    {
        var ss = new SequenceSet();
        ss.Remove(1).ShouldBeFalse();
    }

    // Removing a non-member returns false and doesn't change count.
    [Fact]
    public void Remove_NonMember_ReturnsFalse()
    {
        var ss = new SequenceSet();
        ss.Add(5);
        ss.Remove(4).ShouldBeFalse();
        ss.Count.ShouldBe(1);
    }

    // Removing the only element empties the set.
    [Fact]
    public void Remove_SingleElement_EmptiesSet()
    {
        var ss = new SequenceSet();
        ss.Add(7);
        ss.Remove(7).ShouldBeTrue();
        ss.Count.ShouldBe(0);
        ss.IsEmpty.ShouldBeTrue();
        ss.Contains(7).ShouldBeFalse();
    }

    // Removing the left edge of a range trims it.
    [Fact]
    public void Remove_LeftEdge_TrimsRange()
    {
        var ss = new SequenceSet();
        ss.Add(1); ss.Add(2); ss.Add(3);
        ss.RangeCount.ShouldBe(1);

        ss.Remove(1).ShouldBeTrue();
        ss.Count.ShouldBe(2);
        ss.Contains(1).ShouldBeFalse();
        ss.Contains(2).ShouldBeTrue();
        ss.Contains(3).ShouldBeTrue();
        ss.RangeCount.ShouldBe(1); // still one range [2, 3]
    }

    // Removing the right edge of a range trims it.
    [Fact]
    public void Remove_RightEdge_TrimsRange()
    {
        var ss = new SequenceSet();
        ss.Add(1); ss.Add(2); ss.Add(3);

        ss.Remove(3).ShouldBeTrue();
        ss.Count.ShouldBe(2);
        ss.Contains(3).ShouldBeFalse();
        ss.RangeCount.ShouldBe(1); // still [1, 2]
    }

    // Removing the middle element splits a range into two.
    [Fact]
    public void Remove_MiddleElement_SplitsRange()
    {
        var ss = new SequenceSet();
        ss.Add(1); ss.Add(2); ss.Add(3); ss.Add(4); ss.Add(5);
        ss.RangeCount.ShouldBe(1);

        ss.Remove(3).ShouldBeTrue();
        ss.Count.ShouldBe(4);
        ss.Contains(3).ShouldBeFalse();
        ss.Contains(1).ShouldBeTrue();
        ss.Contains(2).ShouldBeTrue();
        ss.Contains(4).ShouldBeTrue();
        ss.Contains(5).ShouldBeTrue();
        ss.RangeCount.ShouldBe(2); // [1,2] and [4,5]
    }

    // -------------------------------------------------------------------------
    // Enumeration
    // -------------------------------------------------------------------------

    // GetEnumerator returns all sequences in ascending order.
    [Fact]
    public void GetEnumerator_ReturnsAscendingOrder()
    {
        var ss = new SequenceSet();
        ss.Add(5); ss.Add(3); ss.Add(1); ss.Add(2); ss.Add(4);

        var list = ss.ToList();
        list.ShouldBe([1, 2, 3, 4, 5]);
    }

    // Enumeration over a compressed range expands correctly.
    [Fact]
    public void GetEnumerator_CompressedRange_ExpandsAll()
    {
        var ss = new SequenceSet();
        for (ulong i = 100; i <= 200; i++)
            ss.Add(i);

        var list = ss.ToList();
        list.Count.ShouldBe(101);
        list[0].ShouldBe(100UL);
        list[^1].ShouldBe(200UL);
    }

    // Enumeration over multiple disjoint ranges returns all in order.
    [Fact]
    public void GetEnumerator_MultipleRanges_AllInOrder()
    {
        var ss = new SequenceSet();
        ss.Add(10); ss.Add(11);
        ss.Add(20); ss.Add(21); ss.Add(22);
        ss.Add(30);

        var list = ss.ToList();
        list.ShouldBe([10UL, 11UL, 20UL, 21UL, 22UL, 30UL]);
    }

    // -------------------------------------------------------------------------
    // Clear
    // -------------------------------------------------------------------------

    [Fact]
    public void Clear_RemovesAll()
    {
        var ss = new SequenceSet();
        ss.Add(1); ss.Add(2); ss.Add(3);
        ss.Clear();
        ss.Count.ShouldBe(0);
        ss.IsEmpty.ShouldBeTrue();
        ss.Contains(1).ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // ToHashSet snapshot
    // -------------------------------------------------------------------------

    [Fact]
    public void ToHashSet_ReturnsAllElements()
    {
        var ss = new SequenceSet();
        ss.Add(1); ss.Add(2); ss.Add(5); ss.Add(6); ss.Add(7);

        var hs = ss.ToHashSet();
        hs.Count.ShouldBe(5);
        hs.ShouldContain(1UL);
        hs.ShouldContain(2UL);
        hs.ShouldContain(5UL);
        hs.ShouldContain(6UL);
        hs.ShouldContain(7UL);
    }

    // -------------------------------------------------------------------------
    // Binary search correctness — sparse insertions
    // -------------------------------------------------------------------------

    // Reference: Go seqset_test.go — large number of non-contiguous sequences.
    [Fact]
    public void Add_Contains_SparseInsertions_AllFound()
    {
        var ss = new SequenceSet();
        var expected = new List<ulong>();
        for (ulong i = 1; i <= 1000; i += 3) // every 3rd: 1, 4, 7, ...
        {
            ss.Add(i);
            expected.Add(i);
        }

        ss.Count.ShouldBe(expected.Count);

        foreach (var seq in expected)
            ss.Contains(seq).ShouldBeTrue($"Expected seq {seq} to be present");

        // Non-members should not appear.
        ss.Contains(2).ShouldBeFalse();
        ss.Contains(3).ShouldBeFalse();
        ss.Contains(999).ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Boundary conditions
    // -------------------------------------------------------------------------

    [Fact]
    public void Add_SequenceZero_Works()
    {
        var ss = new SequenceSet();
        ss.Add(0).ShouldBeTrue();
        ss.Contains(0).ShouldBeTrue();
        ss.Count.ShouldBe(1);
    }

    [Fact]
    public void Add_AdjacentToZero_Merges()
    {
        var ss = new SequenceSet();
        ss.Add(0);
        ss.Add(1);
        ss.RangeCount.ShouldBe(1); // [0, 1]
        ss.Count.ShouldBe(2);
    }

    [Fact]
    public void Add_Remove_RoundTrip()
    {
        var ss = new SequenceSet();
        for (ulong i = 1; i <= 100; i++)
            ss.Add(i);

        // Remove all odd sequences.
        for (ulong i = 1; i <= 100; i += 2)
            ss.Remove(i);

        ss.Count.ShouldBe(50);
        for (ulong i = 2; i <= 100; i += 2)
            ss.Contains(i).ShouldBeTrue();
        for (ulong i = 1; i <= 99; i += 2)
            ss.Contains(i).ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Merging at boundaries of existing ranges (not just single adjacency)
    // -------------------------------------------------------------------------

    [Fact]
    public void Add_BridgesMultipleGaps_CorrectState()
    {
        var ss = new SequenceSet();
        // Create three separate ranges: [1,2], [4,5], [7,8]
        ss.Add(1); ss.Add(2);
        ss.Add(4); ss.Add(5);
        ss.Add(7); ss.Add(8);
        ss.RangeCount.ShouldBe(3);
        ss.Count.ShouldBe(6);

        // Fill gap between [1,2] and [4,5]: add 3
        ss.Add(3);
        ss.RangeCount.ShouldBe(2); // [1,5] and [7,8]
        ss.Count.ShouldBe(7);

        // Fill gap between [1,5] and [7,8]: add 6
        ss.Add(6);
        ss.RangeCount.ShouldBe(1); // [1,8]
        ss.Count.ShouldBe(8);
    }
}
