using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;

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

public sealed class StreamLifecycleGroupBTests
{
    [Fact]
    public void MaxMsgSize_UsesConfiguredLimit()
    {
        var stream = CreateStream(new StreamConfig
        {
            Name = "ORDERS",
            Subjects = ["orders.*"],
            Storage = StorageType.MemoryStorage,
            MaxMsgSize = 1024,
        });

        stream.MaxMsgSize().ShouldBeGreaterThan(1024UL);
    }

    [Fact]
    public void AutoTuneFileStorageBlockSize_WithMaxMsgsPer_UsesKvDefault()
    {
        var stream = CreateStream(new StreamConfig
        {
            Name = "KV",
            Subjects = ["kv.>"],
            Storage = StorageType.MemoryStorage,
            MaxMsgsPer = 10,
        });

        var cfg = new FileStoreConfig();
        stream.AutoTuneFileStorageBlockSize(cfg);
        cfg.BlockSize.ShouldBe(FileStoreDefaults.DefaultKvBlockSize);
    }

    [Fact]
    public void ClusterSequenceHelpers_RoundTripValues()
    {
        var stream = CreateStream();
        stream.SetCLFS(4);
        stream.SetLastSeq(10);

        stream.GetCLFS().ShouldBe(4UL);
        stream.LastSeqValue().ShouldBe(10UL);
        stream.LastSeqAndCLFS().ShouldBe((10UL, 4UL));
    }

    [Fact]
    public void CreatedTime_SetCreatedTime_UpdatesValue()
    {
        var stream = CreateStream();
        var timestamp = DateTime.UtcNow.AddMinutes(-5);

        stream.SetCreatedTime(timestamp);

        stream.CreatedTime().ShouldBe(timestamp);
    }

    [Fact]
    public void Update_AndUpdatePedantic_ApplyConfig()
    {
        var stream = CreateStream();

        var updated = new StreamConfig { Name = "ORDERS", Subjects = ["orders.v2"], Storage = StorageType.MemoryStorage };
        stream.Update(updated).ShouldBeNull();
        stream.GetConfig().Subjects.ShouldBe(["orders.v2"]);

        var updatedAgain = new StreamConfig { Name = "ORDERS", Subjects = ["orders.v3"], Storage = StorageType.MemoryStorage };
        stream.UpdatePedantic(updatedAgain, pedantic: true).ShouldBeNull();
        stream.GetConfig().Subjects.ShouldBe(["orders.v3"]);
    }

    [Fact]
    public void CheckStreamCfg_NormalizesSubjects_AndRejectsNegativeMaxMsgSize()
    {
        var (server, error) = NatsServer.NewServer(new ServerOptions());
        error.ShouldBeNull();
        server.ShouldNotBeNull();

        var account = new Account { Name = "A" };

        var (normalized, okError) = server.CheckStreamCfg(new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage }, account, pedantic: false);
        okError.ShouldBeNull();
        normalized.Subjects.ShouldBe(["ORDERS.>"]);

        var (_, badError) = server.CheckStreamCfg(new StreamConfig
        {
            Name = "ORDERS",
            Storage = StorageType.MemoryStorage,
            MaxMsgSize = -1,
        }, account, pedantic: false);
        badError.ShouldNotBeNull();
    }

    [Fact]
    public void JsAccount_ConfigUpdateCheck_DetectsInvalidChanges()
    {
        var jsa = new JsAccount();
        var current = new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage };

        jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "DIFFERENT", Storage = StorageType.MemoryStorage }).ShouldNotBeNull();
        jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "ORDERS", Storage = StorageType.FileStorage }).ShouldNotBeNull();
        jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage }).ShouldBeNull();
    }

    [Fact]
    public void JsAccount_SubjectsOverlap_IgnoresOwnAssignment()
    {
        var jsa = new JsAccount();
        var assignment = new StreamAssignment();
        var stream = CreateStream(new StreamConfig
        {
            Name = "ORDERS",
            Subjects = ["orders.*"],
            Storage = StorageType.MemoryStorage,
        });
        stream.SetStreamAssignment(assignment);
        jsa.Streams["ORDERS"] = stream;

        jsa.SubjectsOverlap(["orders.created"], assignment).ShouldBeFalse();
        jsa.SubjectsOverlap(["orders.created"], ownAssignment: null).ShouldBeTrue();
    }

    [Fact]
    public void GroupCHelpers_GetCfgNameAndIsMirror_ReturnExpectedValues()
    {
        var mirrorCfg = new StreamConfig
        {
            Name = "MIRROR",
            Storage = StorageType.MemoryStorage,
            Mirror = new StreamSource { Name = "ORIGIN" },
        };

        var stream = CreateStream(mirrorCfg);

        stream.GetCfgName().ShouldBe("MIRROR");
        stream.IsMirror().ShouldBeTrue();
        stream.MirrorInfo().ShouldBeNull();
    }

    [Fact]
    public void GroupCHelpers_SourceInfoAndBackoff_Behave()
    {
        var stream = CreateStream();
        var info = new StreamSourceInfo
        {
            Name = "SRC",
            FilterSubject = "orders.*",
            Lag = 10,
            Error = "x",
        };

        var cloned = stream.SourceInfo(info);
        cloned.ShouldNotBeNull();
        cloned!.Name.ShouldBe("SRC");
        cloned.FilterSubject.ShouldBe("orders.*");

        NatsStream.CalculateRetryBackoff(1).ShouldBeGreaterThan(TimeSpan.Zero);
        NatsStream.CalculateRetryBackoff(1000).ShouldBe(TimeSpan.FromMinutes(2));
    }

    [Fact]
    public void GroupD_SourceConsumersAndAckParsing_Behave()
    {
        var stream = CreateStream(new StreamConfig
        {
            Name = "ORDERS",
            Subjects = ["orders.*"],
            Storage = StorageType.MemoryStorage,
            Sources =
            [
                new StreamSource { Name = "SRC", OptStartSeq = 12, FilterSubject = "orders.*" },
            ],
        });

        stream.SetupSourceConsumers();
        var source = stream.StreamSource("SRC orders.* >");
        source.ShouldNotBeNull();
        source!.Name.ShouldBe("SRC");
        stream.StartingSequenceForSources(source.IndexName).ShouldBe(12UL);

        stream.SetupSourceConsumer(source.IndexName, 20, DateTime.UtcNow);
        stream.ProcessInboundSourceMsg(source.IndexName, new InMsg { Subject = "orders.created", Msg = "x"u8.ToArray() }).ShouldBeTrue();
        stream.ResetSourceInfo(source.IndexName);
        stream.RetrySourceConsumerAtSeq(source.IndexName, 30);
        stream.StartingSequenceForSources(source.IndexName).ShouldBe(30UL);

        NatsStream.StreamAndSeqFromAckReply("ORDERS.99").ShouldBe(("ORDERS", 99UL));
        NatsStream.StreamAndSeq("A.B.C").ShouldBe(("A", 0UL));
    }

    [Fact]
    public void GroupD_MirrorAndControlPaths_Behave()
    {
        var stream = CreateStream();
        stream.SetupMirrorConsumer();

        stream.ProcessInboundMirrorMsg(new InMsg
        {
            Subject = "$JS.FC.orders",
            Reply = "reply",
            Hdr = Encoding.ASCII.GetBytes("NATS/1.0\r\n\r\n"),
        }).ShouldBeTrue();

        stream.ProcessMirrorMsgs(new StreamSourceInfo { Name = "M", Lag = 2 }, [new InMsg { Subject = "orders.created", Msg = [1] }]);
        stream.RetryDisconnectedSyncConsumers();
    }

    [Fact]
    public void GroupE_SubscriptionsAndInflightCleanup_Behave()
    {
        var stream = CreateStream();

        stream.SubscribeToDirect();
        stream.SubscribeToMirrorDirect();

        var (sub, subErr) = stream.SubscribeInternal("orders.created", handler: null);
        subErr.ShouldBeNull();
        sub.ShouldNotBeNull();

        var (qsub, qErr) = stream.QueueSubscribeInternal("orders.updated", "Q", handler: null);
        qErr.ShouldBeNull();
        qsub.ShouldNotBeNull();
        qsub!.Queue.ShouldNotBeNull();

        stream.UnsubscribeInternal("orders.created").ShouldBeNull();
        stream.RemoveInternalConsumer("orders.updated");

        stream.SubscribeToStream();
        stream.UnsubscribeToStream();

        stream.DeleteInflightBatches(preserveState: false);
        stream.DeleteBatchApplyState();
        stream.StopSourceConsumers();

        stream.UnsubscribeToDirect();
        stream.UnsubscribeToMirrorDirect();
    }

    private static NatsStream CreateStream(StreamConfig? cfg = null)
    {
        cfg ??= new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage };
        return NatsStream.Create(
            new Account { Name = "A" },
            cfg,
            null,
            new JetStreamMemStore(cfg.Clone()),
            null,
            null)!;
    }
}
