// Go reference: server/consumer.go — pauseConsumer / resumeConsumer / isPaused
// Tests for the consumer pause/resume API endpoint, including pause_until (RFC3339)
// time-bounded pauses and response body containing pause state.

using NATS.Server.JetStream.Api;
using NATS.Server.TestUtilities;

namespace NATS.Server.JetStream.Tests.JetStream.Api;

public class ConsumerPauseApiTests : IAsyncLifetime
{
    private JetStreamApiFixture _fx = null!;

    public async Task InitializeAsync()
    {
        _fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
        _ = await _fx.CreateConsumerAsync("ORDERS", "MON", "orders.created");
    }

    public async Task DisposeAsync() => await _fx.DisposeAsync();

    // Go ref: consumer.go pauseConsumer — pause=true pauses consumer.
    [Fact]
    public async Task HandlePause_with_pause_true_pauses_consumer()
    {
        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            "{\"pause\":true}");

        resp.Error.ShouldBeNull();
        resp.Success.ShouldBeTrue();
        resp.Paused.ShouldBe(true);
    }

    // Go ref: consumer.go resumeConsumer — pause=false resumes consumer.
    [Fact]
    public async Task HandlePause_with_pause_false_resumes_consumer()
    {
        // First pause
        await _fx.RequestLocalAsync("$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":true}");

        // Then resume
        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            "{\"pause\":false}");

        resp.Error.ShouldBeNull();
        resp.Success.ShouldBeTrue();
        resp.Paused.ShouldBe(false);
    }

    // Go ref: consumer.go pauseConsumer — pause_until sets deadline UTC datetime.
    [Fact]
    public async Task HandlePause_with_pause_until_sets_deadline()
    {
        var future = DateTime.UtcNow.AddHours(1);
        var iso = future.ToString("O"); // RFC3339 round-trip format

        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            $"{{\"pause_until\":\"{iso}\"}}");

        resp.Error.ShouldBeNull();
        resp.PauseUntil.ShouldNotBeNull();
        resp.PauseUntil!.Value.Should_Be_Close_To_Utc(future, tolerance: TimeSpan.FromSeconds(2));
    }

    // Go ref: consumer.go pauseConsumer — pause_until implies pause=true.
    [Fact]
    public async Task HandlePause_with_pause_until_implies_pause_true()
    {
        var future = DateTime.UtcNow.AddHours(1);
        var iso = future.ToString("O");

        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            $"{{\"pause_until\":\"{iso}\"}}");

        resp.Error.ShouldBeNull();
        resp.Paused.ShouldBe(true);
    }

    // Go ref: consumer.go isPaused — response includes current pause state.
    [Fact]
    public async Task HandlePause_returns_pause_state_in_response()
    {
        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            "{\"pause\":true}");

        resp.Paused.ShouldBe(true);

        var resumeResp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            "{\"pause\":false}");

        resumeResp.Paused.ShouldBe(false);
    }

    // Go ref: consumer.go pauseUntil — response includes pause_until when set.
    [Fact]
    public async Task HandlePause_returns_pause_until_in_response()
    {
        var future = DateTime.UtcNow.AddMinutes(30);
        var iso = future.ToString("O");

        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            $"{{\"pause_until\":\"{iso}\"}}");

        resp.PauseUntil.ShouldNotBeNull();
        resp.PauseUntil!.Value.Kind.ShouldBe(DateTimeKind.Utc);
    }

    // Go ref: consumer.go pauseConsumer — 404 when consumer not found.
    [Fact]
    public async Task HandlePause_returns_not_found_for_missing_consumer()
    {
        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.NONEXISTENT",
            "{\"pause\":true}");

        resp.Error.ShouldNotBeNull();
        resp.Error!.Code.ShouldBe(404);
    }

    // Go ref: consumer.go resumeConsumer — empty payload resumes consumer.
    [Fact]
    public async Task HandlePause_with_empty_payload_resumes()
    {
        // Pause first
        await _fx.RequestLocalAsync("$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":true}");

        // Empty body = resume
        var resp = await _fx.RequestLocalAsync("$JS.API.CONSUMER.PAUSE.ORDERS.MON", "");

        resp.Error.ShouldBeNull();
        resp.Success.ShouldBeTrue();
        resp.Paused.ShouldBe(false);
    }

    // Go ref: consumer.go pauseConsumer — past pause_until auto-resumes immediately.
    [Fact]
    public async Task HandlePause_with_past_pause_until_auto_resumes()
    {
        var past = DateTime.UtcNow.AddHours(-1);
        var iso = past.ToString("O");

        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ORDERS.MON",
            $"{{\"pause_until\":\"{iso}\"}}");

        // Deadline already passed — consumer should auto-resume, so paused=false.
        resp.Error.ShouldBeNull();
        resp.Success.ShouldBeTrue();
        resp.Paused.ShouldBe(false);
    }

    // Go ref: jsConsumerPauseT — bad subject (not matching stream.consumer pattern) returns 404.
    [Fact]
    public async Task HandlePause_returns_not_found_for_bad_subject()
    {
        var resp = await _fx.RequestLocalAsync(
            "$JS.API.CONSUMER.PAUSE.ONLY_ONE_TOKEN",
            "{\"pause\":true}");

        resp.Error.ShouldNotBeNull();
        resp.Error!.Code.ShouldBe(404);
    }
}

/// <summary>
/// Shouldly-compatible extension for DateTime proximity assertions.
/// </summary>
internal static class DateTimeAssertExtensions
{
    public static void Should_Be_Close_To_Utc(this DateTime actual, DateTime expected, TimeSpan tolerance)
    {
        var diff = (actual.ToUniversalTime() - expected.ToUniversalTime()).Duration();
        diff.ShouldBeLessThanOrEqualTo(tolerance);
    }
}
