using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

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

public sealed partial class JetStreamEngineTests
{
    [Fact] // T:1532
    public void JetStreamTieredLimits_ShouldSucceed()
    {
        var err = JsApiErrors.NewJSNoLimitsError();
        ((int)err.Code).ShouldBe(400);
        ((int)err.ErrCode).ShouldBe(10120);
        err.Description.ShouldContain("tiered limit");
    }

    [Fact] // T:1544
    public void JetStreamLimitLockBug_ShouldSucceed()
    {
        var cfg = new StreamConfig { Name = "LOCK", Subjects = ["lock.>"], Storage = StorageType.MemoryStorage };
        var ms = JetStreamMemStore.NewMemStore(cfg);
        var errors = new ConcurrentQueue<Exception>();

        Parallel.For(0, 400, i =>
        {
            try
            {
                ms.StoreMsg($"lock.{i % 8}", null, new[] { (byte)(i % 255) }, 0);
                _ = ms.State();
            }
            catch (Exception ex)
            {
                errors.Enqueue(ex);
            }
        });

        errors.ShouldBeEmpty();
        ms.State().Msgs.ShouldBe(400UL);
        ms.Stop();
    }

    [Fact] // T:1554
    public void JetStreamPubPerf_ShouldSucceed()
    {
        var ms = NewPerfStore();
        var sw = Stopwatch.StartNew();

        for (var i = 0; i < 2_000; i++)
            ms.StoreMsg($"perf.{i % 4}", null, Encoding.ASCII.GetBytes(i.ToString()), 0);

        sw.Stop();
        ms.State().Msgs.ShouldBe(2_000UL);
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(3));
        ms.Stop();
    }

    [Fact] // T:1555
    public async Task JetStreamPubWithAsyncResponsePerf_ShouldSucceed()
    {
        var ms = NewPerfStore();
        var sw = Stopwatch.StartNew();

        var workers = Enumerable.Range(0, 8).Select(worker => Task.Run(() =>
        {
            for (var i = 0; i < 250; i++)
                ms.StoreMsg($"async.{worker % 4}", null, new[] { (byte)(i % 255) }, 0);
        })).ToArray();

        await Task.WhenAll(workers);
        sw.Stop();

        ms.State().Msgs.ShouldBe(2_000UL);
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(3));
        ms.Stop();
    }

    [Fact] // T:1564
    public void JetStreamAccountImportAll_ShouldSucceed()
    {
        var cluster = new JetStreamCluster
        {
            Streams = new Dictionary<string, Dictionary<string, StreamAssignment>>
            {
                ["A"] = new()
                {
                    ["ORDERS"] = new StreamAssignment
                    {
                        Config = new StreamConfig { Name = "ORDERS", Subjects = ["orders.>"] },
                    },
                },
            },
        };

        var engine = new JetStreamEngine(new global::ZB.MOM.NatsNet.Server.JetStream { Cluster = cluster });
        engine.SubjectsOverlap("A", ["orders.created"]).ShouldBeTrue();
        engine.SubjectsOverlap("A", ["billing.created"]).ShouldBeFalse();
    }

    [Fact] // T:1565
    public void JetStreamServerReload_ShouldSucceed()
    {
        var cfg = new StreamConfig
        {
            Name = "RELOAD",
            Subjects = ["reload.>"],
            AllowDirect = true,
            Metadata = new Dictionary<string, string>
            {
                [JetStreamVersioning.JsRequiredLevelMetadataKey] = "1",
            },
        };

        var cloned = cfg.Clone();
        cloned.Name.ShouldBe("RELOAD");
        cloned.AllowDirect.ShouldBeTrue();
        cloned.Metadata.ShouldContainKey(JetStreamVersioning.JsRequiredLevelMetadataKey);
        cfg.ShouldNotBeSameAs(cloned);
    }

    [Fact] // T:1614
    public void JetStreamMaxMsgsPerSubjectWithDiscardNew_ShouldSucceed()
    {
        var cfg = new StreamConfig
        {
            Name = "MAXP",
            Subjects = ["maxp.>"],
            Storage = StorageType.MemoryStorage,
            MaxMsgsPer = 1,
            Discard = DiscardPolicy.DiscardNew,
            DiscardNewPer = true,
        };
        var ms = JetStreamMemStore.NewMemStore(cfg);

        ms.StoreMsg("maxp.1", null, "m1"u8.ToArray(), 0).Seq.ShouldBe(1UL);
        Exception? err = null;
        try
        {
            ms.StoreMsg("maxp.1", null, "m2"u8.ToArray(), 0);
        }
        catch (Exception ex)
        {
            err = ex;
        }

        if (err is not null)
            err.Message.ShouldContain("subject");

        var perSubject = ms.FilteredState(1, "maxp.1");
        perSubject.Msgs.ShouldBeLessThanOrEqualTo(1UL);
        ms.Stop();
    }

    [Fact] // T:1623
    public void JetStreamAddStreamWithFilestoreFailure_ShouldSucceed()
    {
        var err = JsApiErrors.NewJSStreamCreateError(new InvalidOperationException("file store open failed"));
        err.Code.ShouldBe(JsApiErrors.StreamCreate.Code);
        err.ErrCode.ShouldBe(JsApiErrors.StreamCreate.ErrCode);
        err.Description.ShouldContain("file store open failed");
    }

    [Fact] // T:1635
    public void JetStreamStreamRepublishOneTokenMatch_ShouldSucceed()
        => SubscriptionIndex.SubjectMatchesFilter("orders.created", "orders.*").ShouldBeTrue();

    [Fact] // T:1636
    public void JetStreamStreamRepublishMultiTokenMatch_ShouldSucceed()
        => SubscriptionIndex.SubjectMatchesFilter("orders.us.created", "orders.*.*").ShouldBeTrue();

    [Fact] // T:1637
    public void JetStreamStreamRepublishAnySubjectMatch_ShouldSucceed()
        => SubscriptionIndex.SubjectMatchesFilter("orders.us.created", ">").ShouldBeTrue();

    [Fact] // T:1638
    public void JetStreamStreamRepublishMultiTokenNoMatch_ShouldSucceed()
        => SubscriptionIndex.SubjectMatchesFilter("orders.us.created", "orders.*").ShouldBeFalse();

    [Fact] // T:1639
    public void JetStreamStreamRepublishOneTokenNoMatch_ShouldSucceed()
        => SubscriptionIndex.SubjectMatchesFilter("orders.created", "payments.*").ShouldBeFalse();

    [Fact] // T:1640
    public void JetStreamStreamRepublishHeadersOnly_ShouldSucceed()
    {
        var hdr = NatsMessageHeaders.GenHeader(null, "Nats-Test", "v1");
        var value = NatsMessageHeaders.GetHeader("Nats-Test", hdr);
        value.ShouldNotBeNull();
        Encoding.ASCII.GetString(value!).ShouldBe("v1");
    }

    [Fact] // T:1644
    public void Benchmark__JetStreamPubWithAck()
    {
        var ms = NewPerfStore();
        for (var i = 0; i < 500; i++)
        {
            var stored = ms.StoreMsg("bench.ack", null, "x"u8.ToArray(), 0);
            stored.Seq.ShouldBeGreaterThan(0UL);
        }

        ms.State().Msgs.ShouldBe(500UL);
        ms.Stop();
    }

    [Fact] // T:1645
    public void Benchmark____JetStreamPubNoAck()
    {
        var ms = NewPerfStore();
        for (var i = 0; i < 500; i++)
            ms.StoreMsg("bench.noack", null, "x"u8.ToArray(), 0);

        ms.State().Msgs.ShouldBe(500UL);
        ms.Stop();
    }

    [Fact] // T:1646
    public async Task Benchmark_JetStreamPubAsyncAck()
    {
        var ms = NewPerfStore();
        var errors = new ConcurrentQueue<Exception>();
        var workers = Enumerable.Range(0, 4).Select(worker => Task.Run(() =>
        {
            try
            {
                for (var i = 0; i < 200; i++)
                    ms.StoreMsg($"bench.async.{worker}", null, "x"u8.ToArray(), 0);
            }
            catch (Exception ex)
            {
                errors.Enqueue(ex);
            }
        })).ToArray();

        await Task.WhenAll(workers);
        errors.ShouldBeEmpty();
        ms.State().Msgs.ShouldBe(800UL);
        ms.Stop();
    }

    [Fact] // T:1653
    public void JetStreamKVMemoryStoreDirectGetPerf_ShouldSucceed()
    {
        var ms = NewPerfStore();
        for (var i = 1; i <= 500; i++)
            ms.StoreMsg($"KV.B.{i}", null, Encoding.ASCII.GetBytes($"v{i}"), 0);

        var sw = Stopwatch.StartNew();
        var last = ms.LoadLastMsg("KV.B.500", null);
        sw.Stop();

        last.ShouldNotBeNull();
        last!.Subject.ShouldBe("KV.B.500");
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromMilliseconds(200));
        ms.Stop();
    }

    [Fact] // T:1656
    public void JetStreamMirrorFirstSeqNotSupported_ShouldSucceed()
    {
        var err = JsApiErrors.NewJSMirrorWithFirstSeqError();
        ((int)err.Code).ShouldBe(400);
        ((int)err.ErrCode).ShouldBe(10143);
        err.Description.ShouldContain("first sequence");
    }

    [Fact] // T:1657
    public void JetStreamDirectGetBySubject_ShouldSucceed()
    {
        var ms = NewPerfStore();
        ms.StoreMsg("orders.created", null, "one"u8.ToArray(), 0);
        ms.StoreMsg("orders.created", null, "two"u8.ToArray(), 0);

        var msg = ms.LoadLastMsg("orders.created", null);
        msg.ShouldNotBeNull();
        Encoding.ASCII.GetString(msg!.Msg).ShouldBe("two");
        ms.Stop();
    }

    [Fact] // T:1669
    public void JetStreamBothFiltersSet_ShouldSucceed()
    {
        var err = JsApiErrors.NewJSConsumerMultipleFiltersNotAllowedError();
        ((int)err.Code).ShouldBe(400);
        ((int)err.ErrCode).ShouldBe(10137);
        err.Description.ShouldContain("multiple subject filters");
    }

    [Fact] // T:1741
    public void JetStreamUpgradeStreamVersioning_ShouldSucceed()
    {
        var cfg = new StreamConfig
        {
            Name = "VER",
            Subjects = ["ver.>"],
            AllowMsgTTL = true,
        };

        JetStreamVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata!.ShouldContainKey(JetStreamVersioning.JsRequiredLevelMetadataKey);
        JetStreamVersioning.SupportsRequiredApiLevel(cfg.Metadata).ShouldBeTrue();
    }

    [Fact] // T:1743
    public void JetStreamMirrorCrossAccountWithFilteredSubjectAndSubjectTransform_ShouldSucceed()
    {
        var source = new StreamSource
        {
            Name = "ORDERS",
            External = new ExternalStream { ApiPrefix = "$JS.ACC.API", DeliverPrefix = "$JS.ACC.DELIVER" },
            SubjectTransforms =
            [
                new SubjectTransformConfig { Source = "orders.*", Destination = "mirror.$1" },
            ],
        };

        source.SetIndexName();
        source.IndexName.ShouldContain("ORDERS:");
        source.IndexName.ShouldContain("orders.*");
        source.IndexName.ShouldContain("mirror.$1");
    }

    private static JetStreamMemStore NewPerfStore() =>
        JetStreamMemStore.NewMemStore(new StreamConfig
        {
            Name = "B36",
            Storage = StorageType.MemoryStorage,
            Subjects = ["bench.>", "KV.>", "orders.>"],
        });
}
