// MQTT flapper detection tests — exponential backoff for rapid reconnectors.
// Go reference: golang/nats-server/server/mqtt.go mqttCheckFlapper ~lines 300–360.

using NATS.Server.Mqtt;
using Shouldly;

namespace NATS.Server.Mqtt.Tests.Mqtt;

public class MqttFlapperDetectionTests
{
    // -------------------------------------------------------------------------
    // Helper: FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing
    // -------------------------------------------------------------------------

    private static MqttSessionStore CreateStore(
        FakeTimeProvider? time = null,
        int flapThreshold = 3,
        TimeSpan? flapWindow = null) =>
        new(
            flapWindow: flapWindow ?? TimeSpan.FromSeconds(10),
            flapThreshold: flapThreshold,
            timeProvider: time);

    // -------------------------------------------------------------------------
    // 1. TrackConnectDisconnect_counts_events
    // -------------------------------------------------------------------------

    [Fact]
    public void TrackConnectDisconnect_counts_events()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — each connect increments the counter.
        var store = CreateStore();

        var s1 = store.TrackConnectDisconnect("client-a");
        s1.ConnectDisconnectCount.ShouldBe(1);

        var s2 = store.TrackConnectDisconnect("client-a");
        s2.ConnectDisconnectCount.ShouldBe(2);
    }

    // -------------------------------------------------------------------------
    // 2. Not_flapper_below_threshold
    // -------------------------------------------------------------------------

    [Fact]
    public void Not_flapper_below_threshold()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — threshold is 3; 2 events should not mark as flapper.
        var store = CreateStore();

        store.TrackConnectDisconnect("client-b");
        store.TrackConnectDisconnect("client-b");

        store.IsFlapper("client-b").ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // 3. Becomes_flapper_at_threshold
    // -------------------------------------------------------------------------

    [Fact]
    public void Becomes_flapper_at_threshold()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — 3 events within window marks the client.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = CreateStore(time);

        store.TrackConnectDisconnect("client-c");
        time.Advance(TimeSpan.FromSeconds(1));
        store.TrackConnectDisconnect("client-c");
        time.Advance(TimeSpan.FromSeconds(1));
        store.TrackConnectDisconnect("client-c");

        store.IsFlapper("client-c").ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // 4. Backoff_increases_exponentially
    // -------------------------------------------------------------------------

    [Fact]
    public void Backoff_increases_exponentially()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — backoff doubles on each new flap trigger.
        // Level 0 → 1 s, Level 1 → 2 s, Level 2 → 4 s.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = CreateStore(time);

        // First flap at level 0 (1 s backoff)
        store.TrackConnectDisconnect("client-d");
        store.TrackConnectDisconnect("client-d");
        var s1 = store.TrackConnectDisconnect("client-d");
        s1.BackoffLevel.ShouldBe(1); // incremented after applying level 0
        s1.BackoffUntil.ShouldNotBeNull();
        var backoff1 = s1.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
        backoff1.TotalMilliseconds.ShouldBeInRange(900, 1100); // ~1 000 ms

        // Advance past the backoff and trigger again — level 1 (2 s)
        time.Advance(TimeSpan.FromSeconds(2));
        var s2 = store.TrackConnectDisconnect("client-d");
        s2.BackoffLevel.ShouldBe(2);
        var backoff2 = s2.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
        backoff2.TotalMilliseconds.ShouldBeInRange(1900, 2100); // ~2 000 ms

        // Advance past and trigger once more — level 2 (4 s)
        time.Advance(TimeSpan.FromSeconds(3));
        var s3 = store.TrackConnectDisconnect("client-d");
        s3.BackoffLevel.ShouldBe(3);
        var backoff3 = s3.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
        backoff3.TotalMilliseconds.ShouldBeInRange(3900, 4100); // ~4 000 ms
    }

    // -------------------------------------------------------------------------
    // 5. Backoff_capped_at_60_seconds
    // -------------------------------------------------------------------------

    [Fact]
    public void Backoff_capped_at_60_seconds()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — cap the maximum backoff at 60 s.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = CreateStore(time);

        // Trigger enough flaps to overflow past 60 s (level 6 = 64 s, which should cap at 60 s)
        for (var i = 0; i < 10; i++)
        {
            store.TrackConnectDisconnect("client-e");
            time.Advance(TimeSpan.FromMilliseconds(100));
        }

        var state = store.TrackConnectDisconnect("client-e");
        var remaining = state.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
        remaining.TotalMilliseconds.ShouldBeLessThanOrEqualTo(60_001); // max 60 s (±1 ms tolerance)
        remaining.TotalMilliseconds.ShouldBeGreaterThan(0);
    }

    // -------------------------------------------------------------------------
    // 6. GetBackoffMs_returns_remaining
    // -------------------------------------------------------------------------

    [Fact]
    public void GetBackoffMs_returns_remaining()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — caller can query remaining backoff time.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = CreateStore(time);

        store.TrackConnectDisconnect("client-f");
        store.TrackConnectDisconnect("client-f");
        store.TrackConnectDisconnect("client-f"); // threshold hit

        var ms = store.GetBackoffMs("client-f");
        ms.ShouldBeGreaterThan(0);
        ms.ShouldBeLessThanOrEqualTo(1000);
    }

    // -------------------------------------------------------------------------
    // 7. GetBackoffMs_zero_when_not_flapping
    // -------------------------------------------------------------------------

    [Fact]
    public void GetBackoffMs_zero_when_not_flapping()
    {
        // Not enough events to trigger backoff — remaining ms should be 0.
        var store = CreateStore();

        store.TrackConnectDisconnect("client-g");
        store.TrackConnectDisconnect("client-g");

        store.GetBackoffMs("client-g").ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // 8. ClearFlapperState_removes_tracking
    // -------------------------------------------------------------------------

    [Fact]
    public void ClearFlapperState_removes_tracking()
    {
        // Go reference: server/mqtt.go — stable clients should have state purged.
        var store = CreateStore();

        store.TrackConnectDisconnect("client-h");
        store.TrackConnectDisconnect("client-h");
        store.TrackConnectDisconnect("client-h");
        store.IsFlapper("client-h").ShouldBeTrue();

        store.ClearFlapperState("client-h");

        store.IsFlapper("client-h").ShouldBeFalse();
        store.GetBackoffMs("client-h").ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // 9. Window_resets_after_10_seconds
    // -------------------------------------------------------------------------

    [Fact]
    public void Window_resets_after_10_seconds()
    {
        // Go reference: server/mqtt.go mqttCheckFlapper — window-based detection resets.
        // Track 2 events, advance past the window, add 1 more — should NOT be a flapper.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = CreateStore(time);

        store.TrackConnectDisconnect("client-i");
        store.TrackConnectDisconnect("client-i");

        // Advance past the 10 s flap window
        time.Advance(TimeSpan.FromSeconds(11));

        // Directly set the WindowStart via the returned state to simulate the old window
        // being in the past. A single new event in a new window should not cross threshold.
        store.TrackConnectDisconnect("client-i");

        store.IsFlapper("client-i").ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // 10. CheckAndClearStableClients_clears_old
    // -------------------------------------------------------------------------

    [Fact]
    public void CheckAndClearStableClients_clears_old()
    {
        // Go reference: server/mqtt.go — periodic sweep clears long-stable flapper records.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = CreateStore(time);

        // Trigger flap
        store.TrackConnectDisconnect("client-j");
        store.TrackConnectDisconnect("client-j");
        var state = store.TrackConnectDisconnect("client-j");
        store.IsFlapper("client-j").ShouldBeTrue();

        // Manually backdate BackoffUntil so it's already expired
        lock (state)
        {
            state.BackoffUntil = time.GetUtcNow().UtcDateTime - TimeSpan.FromSeconds(61);
        }

        // A stable-threshold sweep of 60 s should evict the now-expired entry
        store.CheckAndClearStableClients(TimeSpan.FromSeconds(60));

        store.IsFlapper("client-j").ShouldBeFalse();
        store.GetBackoffMs("client-j").ShouldBe(0);
    }
}
