// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server;

namespace ZB.MOM.NatsNet.Server.Tests.JetStream;

public sealed partial class NatsConsumerTests
{
    [Fact] // T:1304
    public void JetStreamConsumerAndStreamNamesWithPathSeparators_ShouldSucceed()
    {
        var streamErr = JsApiErrors.NewJSStreamNameContainsPathSeparatorsError();
        streamErr.Code.ShouldBe(JsApiErrors.StreamNameContainsPathSeparators.Code);
        streamErr.ErrCode.ShouldBe(JsApiErrors.StreamNameContainsPathSeparators.ErrCode);
        streamErr.Description.ShouldBe("Stream name can not contain path separators");

        var consumerErr = JsApiErrors.NewJSConsumerNameContainsPathSeparatorsError();
        consumerErr.Code.ShouldBe(JsApiErrors.ConsumerNameContainsPathSeparators.Code);
        consumerErr.ErrCode.ShouldBe(JsApiErrors.ConsumerNameContainsPathSeparators.ErrCode);
        consumerErr.Description.ShouldBe("Consumer name can not contain path separators");
    }

    [Fact]
    public void Create_SetLeader_UpdateConfig_AndStop_ShouldBehave()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Storage = StorageType.FileStorage };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
        var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.CreateOrUpdate, null);
        consumer.ShouldNotBeNull();

        consumer!.IsLeader().ShouldBeFalse();
        consumer.SetLeader(true, 3);
        consumer.IsLeader().ShouldBeTrue();

        var updated = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll };
        consumer.UpdateConfig(updated);
        consumer.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll);

        var info = consumer.GetInfo();
        info.Stream.ShouldBe("S");
        info.Name.ShouldBe("D");

        consumer.Stop();
        consumer.IsLeader().ShouldBeFalse();
    }

    [Fact] // T:1364
    public void SortingConsumerPullRequests_ShouldSucceed()
    {
        var q = new WaitQueue(max: 100);

        q.AddPrioritized(new WaitingRequest { Reply = "1a", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "2a", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "1b", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "2b", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "1c", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "3a", PriorityGroup = new PriorityGroup { Priority = 3 }, N = 1 })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "2c", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 })
            .ShouldBeTrue();

        var expectedOrder = new[]
        {
            ("1a", 1),
            ("1b", 1),
            ("1c", 1),
            ("2a", 2),
            ("2b", 2),
            ("2c", 2),
            ("3a", 3),
        };

        q.Len.ShouldBe(expectedOrder.Length);
        foreach (var (reply, priority) in expectedOrder)
        {
            var current = q.Peek();
            current.ShouldNotBeNull();
            current!.Reply.ShouldBe(reply);
            current.PriorityGroup.ShouldNotBeNull();
            current.PriorityGroup!.Priority.ShouldBe(priority);
            q.RemoveCurrent();
        }

        q.IsEmpty().ShouldBeTrue();
    }

    [Fact] // T:1365
    public void WaitQueuePopAndRequeue_ShouldSucceed()
    {
        var q = new WaitQueue(max: 100);
        q.AddPrioritized(new WaitingRequest { Reply = "1a", N = 2, PriorityGroup = new PriorityGroup { Priority = 1 } })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "1b", N = 1, PriorityGroup = new PriorityGroup { Priority = 1 } })
            .ShouldBeTrue();
        q.AddPrioritized(new WaitingRequest { Reply = "2a", N = 3, PriorityGroup = new PriorityGroup { Priority = 2 } })
            .ShouldBeTrue();

        var wr = q.PopAndRequeue();
        wr.ShouldNotBeNull();
        wr!.Reply.ShouldBe("1a");
        wr.N.ShouldBe(1);
        q.Len.ShouldBe(3);

        wr = q.PopAndRequeue();
        wr.ShouldNotBeNull();
        wr!.Reply.ShouldBe("1b");
        wr.N.ShouldBe(0);
        q.Len.ShouldBe(2);

        wr = q.PopAndRequeue();
        wr.ShouldNotBeNull();
        wr!.Reply.ShouldBe("1a");
        wr.N.ShouldBe(0);
        q.Len.ShouldBe(1);

        q.Peek()!.Reply.ShouldBe("2a");
        q.Peek()!.N.ShouldBe(3);
    }

    [Fact]
    public void AddConsumerWithAction_CreateThenUpdate_ShouldRespectActions()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
        var (created, createErr) = stream!.AddConsumerWithAction(cfg, "D", ConsumerAction.Create, pedantic: false);
        createErr.ShouldBeNull();
        created.ShouldNotBeNull();

        var updateCfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll };
        var (updated, updateErr) = stream.AddConsumerWithAction(updateCfg, "D", ConsumerAction.Update, pedantic: false);
        updateErr.ShouldBeNull();
        updated.ShouldNotBeNull();
        updated!.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll);
    }

    [Fact]
    public void AddConsumer_WithAssignment_ShouldAttachAssignment()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var assignment = new ConsumerAssignment
        {
            Name = "D",
            Stream = "S",
            Group = new RaftGroup { Name = "RG", Peers = ["N1"] },
        };
        var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };

        var (consumer, err) = stream!.AddConsumerWithAssignment(cfg, "D", assignment, isRecovering: false, ConsumerAction.Create, pedantic: false);
        err.ShouldBeNull();
        consumer.ShouldNotBeNull();
        consumer!.ConsumerAssignment().ShouldBeSameAs(assignment);
    }

    [Fact]
    public void UpdateInactiveThreshold_AndPauseState_ShouldTrackConfigValues()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
        var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        consumer!.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromSeconds(30) });
        consumer.GetConfig().InactiveThreshold.ShouldBe(TimeSpan.FromSeconds(30));

        var pauseUntil = DateTime.UtcNow.AddMinutes(1);
        consumer.UpdatePauseState(new ConsumerConfig { PauseUntil = pauseUntil });
        consumer.GetConfig().PauseUntil.ShouldBe(pauseUntil);
    }

    [Fact]
    public void ConsumerAssignment_GetSet_ShouldRoundTrip()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
        var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        var assignment = new ConsumerAssignment
        {
            Name = "D",
            Stream = "S",
            Group = new RaftGroup { Name = "RG", Peers = ["N1"] },
        };

        consumer!.SetConsumerAssignment(assignment);
        consumer.ConsumerAssignment().ShouldBeSameAs(assignment);
    }

    [Fact]
    public async Task MonitorQuitC_AndSignalMonitorQuit_ShouldPublishQuitSignal()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        var monitor = consumer!.MonitorQuitC();
        monitor.ShouldNotBeNull();
        monitor!.TryRead(out _).ShouldBeFalse();

        consumer.SignalMonitorQuit();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
        var signal = await monitor.ReadAsync(cts.Token);
        signal.ShouldBeTrue();
    }

    [Fact]
    public void SubscribeInternal_Unsubscribe_AndHasDeliveryInterest_ShouldTrackState()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(
            stream!,
            new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo" },
            ConsumerAction.Create,
            null);
        consumer.ShouldNotBeNull();

        consumer!.HasDeliveryInterest().ShouldBeFalse();
        consumer.SubscribeInternal("deliver.foo").ShouldBeTrue();
        consumer.CheckQueueInterest("deliver.foo").ShouldBeTrue();
        consumer.HasDeliveryInterest().ShouldBeTrue();
        consumer.Unsubscribe("deliver.foo").ShouldBeTrue();
        consumer.HasDeliveryInterest().ShouldBeFalse();
    }

    [Fact]
    public void AdvisoryHelpers_AndCreatedTime_ShouldBehave()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        consumer!.SendCreateAdvisory().ShouldBeTrue();
        consumer.SendDeleteAdvisoryLocked().ShouldBeTrue();
        consumer.SendPinnedAdvisoryLocked("pin-1").ShouldBeTrue();
        consumer.SendUnpinnedAdvisoryLocked("pin-1").ShouldBeTrue();
        consumer.SendPauseAdvisoryLocked(DateTime.UtcNow.AddMinutes(1)).ShouldBeTrue();

        var created = DateTime.UtcNow.AddHours(-1);
        consumer.SetCreatedTime(created);
        consumer.CreatedTime().ShouldBe(created);
    }

    [Fact]
    public void HandleClusterConsumerInfoRequest_WhenLeader_ReturnsInfo()
    {
        var account = new Account { Name = "A" };
        var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
        var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        consumer!.HandleClusterConsumerInfoRequest().ShouldBeNull();
        consumer.SetLeader(true, 1);
        consumer.IsLeaderInternal().ShouldBeTrue();
        consumer.HandleClusterConsumerInfoRequest().ShouldNotBeNull();
        consumer.ClearNode();
    }

    [Fact]
    public void UpdateDeliveryInterest_AndDeleteNotActive_ShouldReflectInterestState()
    {
        var account = new Account { Name = "A" };
        var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(
            stream!,
            new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo", InactiveThreshold = TimeSpan.FromMilliseconds(20) },
            ConsumerAction.Create,
            null);
        consumer.ShouldNotBeNull();

        consumer!.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromMilliseconds(20) });
        consumer.UpdateDeliveryInterest(localInterest: false).ShouldBeTrue();
        Thread.Sleep(40);
        consumer.IsClosed().ShouldBeTrue();
    }

    [Fact]
    public void WatchGWinterest_AndRateLimit_ShouldExecuteWithoutErrors()
    {
        var account = new Account { Name = "A" };
        var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"], MaxMsgSize = 4096 }, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(
            stream!,
            new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo", RateLimit = 8_000 },
            ConsumerAction.Create,
            null);
        consumer.ShouldNotBeNull();

        consumer!.SetRateLimitNeedsLocks();
        consumer.WatchGWinterest();
        consumer.SubscribeInternal("deliver.foo").ShouldBeTrue();
        consumer.UpdateDeliveryInterest(localInterest: true).ShouldBeFalse();
        consumer.HasDeliveryInterest().ShouldBeTrue();
    }

    [Fact]
    public void AccountCheckNewConsumerConfig_InvalidPolicyChanges_ShouldFail()
    {
        var account = new Account { Name = "A" };
        var current = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit, DeliverPolicy = DeliverPolicy.DeliverAll };
        var next = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll, DeliverPolicy = DeliverPolicy.DeliverAll };

        var err = account.CheckNewConsumerConfig(current, next);

        err.ShouldNotBeNull();
        err.Message.ShouldContain("ack policy");
    }

    [Fact]
    public void UpdateDeliverSubject_AndConfigsEqualSansDelivery_ShouldBehave()
    {
        var account = new Account { Name = "A" };
        var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
        stream.ShouldNotBeNull();

        var cfg = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.a", AckPolicy = AckPolicy.AckExplicit };
        var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        consumer!.SubscribeInternal("deliver.a");
        consumer.UpdateDeliverSubject("deliver.b").ShouldBeTrue();
        consumer.SubscribeInternal("deliver.b");
        consumer.HasDeliveryInterest().ShouldBeTrue();

        var left = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.a", AckPolicy = AckPolicy.AckExplicit };
        var right = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.b", AckPolicy = AckPolicy.AckExplicit };
        NatsConsumer.ConfigsEqualSansDelivery(left, right).ShouldBeTrue();
    }

    [Fact]
    public void AckFlow_NewMessage_Push_Process_Progress_UpdateSkipped_ShouldBehave()
    {
        var account = new Account { Name = "A" };
        var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }, ConsumerAction.Create, null);
        consumer.ShouldNotBeNull();

        var ack = NatsConsumer.NewJSAckMsg("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray());
        ack.Subject.ShouldBe("a.b.10.20.1");

        consumer!.PushAck("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray());
        consumer.ProcessAck("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray());
        consumer.ProgressUpdate(10);
        consumer.UpdateSkipped(25);

        var state = consumer.GetConsumerState();
        state.AckFloor.Stream.ShouldBeGreaterThanOrEqualTo(10UL);
    }

    [Fact]
    public void HasMaxDeliveries_ForceExpirePending_AndResetStartingSeq_ShouldBehave()
    {
        var account = new Account { Name = "A" };
        var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
        stream.ShouldNotBeNull();

        var consumer = NatsConsumer.Create(
            stream!,
            new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit, MaxDeliver = 2, DeliverPolicy = DeliverPolicy.DeliverAll },
            ConsumerAction.Create,
            null);
        consumer.ShouldNotBeNull();

        consumer!.ProcessAck("a.b.5.7.1", "reply", 0, "-NAK"u8.ToArray());
        consumer.HasMaxDeliveries(5).ShouldBeFalse();
        consumer.HasMaxDeliveries(5).ShouldBeTrue();
        consumer.ForceExpirePending();

        var (seq, canRespond, err) = consumer.ResetStartingSeq(10, "reply");
        err.ShouldBeNull();
        seq.ShouldBe(10UL);
        canRespond.ShouldBeTrue();
    }

    [Fact]
    public void JsPubMsg_ReturnToPool_ShouldResetState()
    {
        var msg = new JsPubMsg
        {
            Subject = "foo",
            Reply = "bar",
            Hdr = [1, 2],
            Msg = [3, 4],
            Pa = new object(),
            Sync = new object(),
        };

        msg.ReturnToPool();

        msg.Subject.ShouldBe(string.Empty);
        msg.Reply.ShouldBeNull();
        msg.Hdr.ShouldBeNull();
        msg.Msg.ShouldBeNull();
        msg.Pa.ShouldBeNull();
        msg.Sync.ShouldBeNull();
    }
}
