using NSubstitute;
using NATS.Server.JetStream.MirrorSource;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;

namespace NATS.Server.JetStream.Tests;

// Go reference: server/stream.go:3478-3505 (calculateRetryBackoff),
// server/stream.go:3125-3400 (setupMirrorConsumer retry logic)

public class MirrorSourceRetryTests
{
    [Fact]
    public void Mirror_retry_uses_exponential_backoff()
    {
        // Go reference: server/stream.go:3478-3505 calculateRetryBackoff
        var mirror = MirrorCoordinatorTestHelper.Create();

        mirror.RecordFailure();
        var delay1 = mirror.GetRetryDelay();
        delay1.ShouldBeGreaterThanOrEqualTo(TimeSpan.FromMilliseconds(250));

        mirror.RecordFailure();
        var delay2 = mirror.GetRetryDelay();
        delay2.ShouldBeGreaterThan(delay1);

        // Cap at max
        for (int i = 0; i < 20; i++) mirror.RecordFailure();
        var delayMax = mirror.GetRetryDelay();
        delayMax.ShouldBeLessThanOrEqualTo(TimeSpan.FromSeconds(30));
    }

    [Fact]
    public void Mirror_success_resets_backoff()
    {
        // Go reference: server/stream.go setupMirrorConsumer — success resets retry
        var mirror = MirrorCoordinatorTestHelper.Create();

        for (int i = 0; i < 5; i++) mirror.RecordFailure();
        mirror.RecordSuccess();

        var delay = mirror.GetRetryDelay();
        delay.ShouldBe(TimeSpan.FromMilliseconds(250));
    }

    [Fact]
    public void Mirror_tracks_sequence_gap()
    {
        // Go reference: server/stream.go:2863-3014 processInboundMirrorMsg — gap detection
        var mirror = MirrorCoordinatorTestHelper.Create();

        mirror.RecordSourceSeq(1);
        mirror.RecordSourceSeq(2);
        mirror.RecordSourceSeq(5); // gap: 3, 4 missing

        mirror.HasGap.ShouldBeTrue();
        mirror.GapStart.ShouldBe(3UL);
        mirror.GapEnd.ShouldBe(4UL);
    }

    [Fact]
    public void Mirror_tracks_error_state()
    {
        // Go reference: server/stream.go mirror error state tracking
        var mirror = MirrorCoordinatorTestHelper.Create();

        mirror.SetError("connection refused");
        mirror.HasError.ShouldBeTrue();
        mirror.ErrorMessage.ShouldBe("connection refused");

        mirror.ClearError();
        mirror.HasError.ShouldBeFalse();
    }

    [Fact]
    public void Source_dedup_window_prunes_expired()
    {
        // Go reference: server/stream.go duplicate window pruning
        var source = SourceCoordinatorTestHelper.Create();

        source.RecordMsgId("msg-1");
        source.RecordMsgId("msg-2");

        source.IsDuplicate("msg-1").ShouldBeTrue();
        source.IsDuplicate("msg-3").ShouldBeFalse();

        // Simulate time passing beyond dedup window
        source.PruneDedupWindow(DateTimeOffset.UtcNow.AddMinutes(5));
        source.IsDuplicate("msg-1").ShouldBeFalse();
    }
}

public static class MirrorCoordinatorTestHelper
{
    public static MirrorCoordinator Create()
    {
        var store = Substitute.For<IStreamStore>();
        return new MirrorCoordinator(store);
    }
}

public static class SourceCoordinatorTestHelper
{
    public static SourceCoordinator Create()
    {
        var store = Substitute.For<IStreamStore>();
        var config = new StreamSourceConfig { Name = "test-source", DuplicateWindowMs = 60_000 };
        return new SourceCoordinator(store, config);
    }
}
