using System.Reflection;
using System.Text;
using System.Text.Json;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

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

public sealed class EventsHandlerTests
{
    [Fact] // T:299
    public void SystemAccount_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions
        {
            NoSystemAccount = true,
        });
        err.ShouldBeNull();
        server.ShouldNotBeNull();

        server!.SetDefaultSystemAccount().ShouldBeNull();

        var sys = server.SystemAccount();
        var global = server.GlobalAccount();
        sys.ShouldNotBeNull();
        global.ShouldNotBeNull();
        sys!.Name.ShouldBe(ServerConstants.DefaultSystemAccount);
        global!.Name.ShouldBe(ServerConstants.DefaultGlobalAccount);
        sys.Name.ShouldNotBe(global.Name);
    }

    [Fact] // T:300
    public void SystemAccountNewConnection_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
        err.ShouldBeNull();
        server!.SetDefaultSystemAccount().ShouldBeNull();

        var sys = server.SystemAccount();
        sys.ShouldNotBeNull();

        var c = new ClientConnection(ClientKind.Client, server) { Cid = 1001 };
        c.RegisterWithAccount(sys!);

        sys.NumConnections().ShouldBe(1);
    }

    [Fact] // T:301
    public void SystemAccountingWithLeafNodes_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
        err.ShouldBeNull();
        server!.SetDefaultSystemAccount().ShouldBeNull();
        var sys = server.SystemAccount();
        sys.ShouldNotBeNull();

        var leaf = new ClientConnection(ClientKind.Leaf, server) { Cid = 1002 };
        leaf.RegisterWithAccount(sys!);

        sys.NumLeafNodes().ShouldBe(1);
    }

    [Fact] // T:302
    public void SystemAccountDisconnectBadLogin_ShouldSucceed()
    {
        var c = new ClientConnection(ClientKind.Client);
        c.AuthViolation();
        c.IsClosed().ShouldBeTrue();
    }

    [Fact] // T:306
    public void SystemAccountConnectionLimits_ShouldSucceed()
    {
        var acc = Account.NewAccount("SYS");
        acc.MaxConnections = 1;

        var c1 = new ClientConnection(ClientKind.Client) { Cid = 1 };
        var c2 = new ClientConnection(ClientKind.Client) { Cid = 2 };
        c1.RegisterWithAccount(acc);

        Should.Throw<TooManyAccountConnectionsException>(() => c2.RegisterWithAccount(acc));
    }

    [Fact] // T:308
    public void SystemAccountSystemConnectionLimitsHonored_ShouldSucceed()
    {
        var acc = Account.NewAccount("SYS");
        acc.MaxConnections = 1;

        var s1 = new ClientConnection(ClientKind.System) { Cid = 11 };
        var s2 = new ClientConnection(ClientKind.System) { Cid = 12 };
        s1.RegisterWithAccount(acc);
        s2.RegisterWithAccount(acc);

        acc.NumConnections().ShouldBe(0);
    }

    [Fact] // T:309
    public void SystemAccountConnectionLimitsServersStaggered_ShouldSucceed()
    {
        var acc = Account.NewAccount("TEST");
        acc.MaxConnections = 3;

        for (var i = 0; i < 3; i++)
            new ClientConnection(ClientKind.Client) { Cid = (ulong)(20 + i) }.RegisterWithAccount(acc);

        var overByTwo = acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-a", Name = "a" },
            Account = "TEST",
            Conns = 2,
        });
        overByTwo.Count.ShouldBe(2);

        var overByOne = acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-a", Name = "a" },
            Account = "TEST",
            Conns = 1,
        });
        overByOne.Count.ShouldBe(1);
    }

    [Fact] // T:310
    public void SystemAccountConnectionLimitsServerShutdownGraceful_ShouldSucceed()
    {
        var acc = Account.NewAccount("TEST");
        acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-a", Name = "a" },
            Account = "TEST",
            Conns = 1,
        });
        acc.ExpectedRemoteResponses().ShouldBe(1);

        acc.RemoveRemoteServer("srv-a");
        acc.ExpectedRemoteResponses().ShouldBe(0);
    }

    [Fact] // T:311
    public void SystemAccountConnectionLimitsServerShutdownForced_ShouldSucceed()
    {
        var acc = Account.NewAccount("TEST");
        acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-a", Name = "a" },
            Account = "TEST",
            Conns = 2,
        });

        acc.RemoveRemoteServer("srv-missing");
        acc.ExpectedRemoteResponses().ShouldBe(1);

        acc.RemoveRemoteServer("srv-a");
        acc.ExpectedRemoteResponses().ShouldBe(0);
    }

    [Fact] // T:312
    public void SystemAccountFromConfig_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions
        {
            Accounts = [new Account { Name = "SYSCFG" }],
            SystemAccount = "SYSCFG",
        });
        err.ShouldBeNull();
        server.ShouldNotBeNull();
        server!.SystemAccount().ShouldNotBeNull();
        server.SystemAccount()!.Name.ShouldBe("SYSCFG");
    }

    [Fact] // T:313
    public void AccountClaimsUpdates_ShouldSucceed()
    {
        AccountClaims.TryDecode(string.Empty).ShouldBeNull();
        AccountClaims.TryDecode("not-a-real-jwt").ShouldBeNull();
    }

    [Fact] // T:315
    public void AccountReqInfo_ShouldSucceed()
    {
        var acc = Account.NewAccount("A");
        var id1 = acc.NextEventId();
        var id2 = acc.NextEventId();

        id1.ShouldNotBeNullOrWhiteSpace();
        id2.ShouldNotBeNullOrWhiteSpace();
        id1.ShouldNotBe(id2);
    }

    [Fact] // T:316
    public void AccountClaimsUpdatesWithServiceImports_ShouldSucceed()
    {
        var source = Account.NewAccount("src");
        var original = Account.NewAccount("dst");
        original.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["svc"] = [new ServiceImportEntry { Account = source, From = "svc", To = "svc" }],
        };

        var updated = Account.NewAccount("dst");
        original.ShallowCopy(updated);

        updated.Imports.Services.ShouldNotBeNull();
        updated.Imports.Services!.ShouldContainKey("svc");
        updated.Imports.Services["svc"].Count.ShouldBe(1);
    }

    [Fact] // T:317
    public void AccountConnsLimitExceededAfterUpdate_ShouldSucceed()
    {
        var acc = Account.NewAccount("A");
        acc.MaxConnections = 2;
        new ClientConnection(ClientKind.Client) { Cid = 71 }.RegisterWithAccount(acc);
        new ClientConnection(ClientKind.Client) { Cid = 72 }.RegisterWithAccount(acc);

        var toDisconnect = acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-b", Name = "b" },
            Account = "A",
            Conns = 2,
        });

        toDisconnect.Count.ShouldBe(2);
    }

    [Fact] // T:320
    public void SystemAccountWithGateways_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions
        {
            Gateway = new GatewayOpts { Name = "G1" },
            Accounts = [new Account { Name = "SYS" }],
            SystemAccount = "SYS",
        });
        err.ShouldBeNull();
        server.ShouldNotBeNull();
        server!.GetOpts().Gateway.Name.ShouldBe("G1");
        server.SystemAccount()!.Name.ShouldBe("SYS");
    }

    [Fact] // T:321
    public void SystemAccountNoAuthUser_ShouldSucceed()
    {
        var opts = new ServerOptions
        {
            Users = [new User { Username = "noauth" }],
            NoAuthUser = "noauth",
            SystemAccount = "SYS",
        };

        AuthHandler.ValidateNoAuthUser(opts, opts.NoAuthUser).ShouldBeNull();
    }

    [Fact] // T:322
    public void ServerAccountConns_ShouldSucceed()
    {
        var acc = Account.NewAccount("A");
        var c = new ClientConnection(ClientKind.Client) { Cid = 81 };
        c.RegisterWithAccount(acc);
        acc.NumConnections().ShouldBe(1);

        ((INatsAccount)acc).RemoveClient(c);
        acc.NumConnections().ShouldBe(0);
    }

    [Fact] // T:323
    public void ServerEventsStatsZ_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();

        server!.NumSlowConsumers().ShouldBe(0);
        server.NumStaleConnections().ShouldBe(0);
        server.NumClients().ShouldBeGreaterThanOrEqualTo(0);
    }

    [Fact] // T:324
    public void ServerEventsHealthZSingleServer_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();

        server!.NumRoutes().ShouldBe(0);
        server.NumRemotes().ShouldBe(0);
    }

    [Fact] // T:327
    public void ServerEventsHealthZJetStreamNotEnabled_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();
        server.ShouldNotBeNull();
        server!.GetOpts().JetStream.ShouldBeFalse();
    }

    [Fact] // T:328
    public void ServerEventsPingStatsZ_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();

        server!.NumSlowConsumersClients().ShouldBe(0);
        server.NumSlowConsumersRoutes().ShouldBe(0);
        server.NumSlowConsumersGateways().ShouldBe(0);
        server.NumSlowConsumersLeafs().ShouldBe(0);
    }

    [Fact] // T:329
    public void ServerEventsPingStatsZDedicatedRecvQ_ShouldSucceed()
    {
        var server = CreateServer();
        var sc = GetPrivateField<SlowConsumerStats>(server, "_scStats");

        sc.Clients = 1;
        sc.Routes = 2;
        sc.Gateways = 3;
        sc.Leafs = 4;

        server.NumSlowConsumersClients().ShouldBe(1);
        server.NumSlowConsumersRoutes().ShouldBe(2);
        server.NumSlowConsumersGateways().ShouldBe(3);
        server.NumSlowConsumersLeafs().ShouldBe(4);
    }

    [Fact] // T:330
    public void ServerEventsPingStatsZFilter_ShouldSucceed()
    {
        var server = CreateServer(new ServerOptions
        {
            Host = "127.0.0.1",
            ServerName = "SRV",
            Cluster = new ClusterOpts { Name = "CLUSTER" },
        });

        var info = server.CopyInfo();
        MatchesServerFilter(info, cluster: "CLUSTER").ShouldBeTrue();
        MatchesServerFilter(info, host: "127.0.0.1").ShouldBeTrue();
        MatchesServerFilter(info, name: "SRV").ShouldBeTrue();
        MatchesServerFilter(info, cluster: "OTHER").ShouldBeFalse();
        MatchesServerFilter(info, host: "bad-host").ShouldBeFalse();
        MatchesServerFilter(info, name: "bad-name").ShouldBeFalse();
    }

    [Fact] // T:331
    public void ServerEventsPingStatsZFailFilter_ShouldSucceed()
    {
        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<Dictionary<string, object>>("{MALFORMEDJSON"));

        var ok = JsonSerializer.Deserialize<Dictionary<string, object>>("{\"cluster\":\"DOESNOTEXIST\"}");
        ok.ShouldNotBeNull();
        ok!.ShouldContainKey("cluster");
    }

    [Fact] // T:332
    public void ServerEventsPingMonitorz_ShouldSucceed()
    {
        var paths = new[]
        {
            NatsServer.MonitorPaths.Varz,
            NatsServer.MonitorPaths.Subsz,
            NatsServer.MonitorPaths.Connz,
            NatsServer.MonitorPaths.Routez,
            NatsServer.MonitorPaths.Gatewayz,
            NatsServer.MonitorPaths.Leafz,
            NatsServer.MonitorPaths.Accountz,
            NatsServer.MonitorPaths.Healthz,
            NatsServer.MonitorPaths.Expvarz,
        };

        foreach (var path in paths)
        {
            path.ShouldStartWith("/");
            path.Length.ShouldBeGreaterThan(1);
        }
    }

    [Fact] // T:335
    public void ServerEventsReceivedByQSubs_ShouldSucceed()
    {
        var sublist = SubscriptionIndex.NewSublistWithCache();
        var subject = "$SYS.SERVER.*.CLIENT.AUTH.ERR";
        var queue = Encoding.UTF8.GetBytes("queue");

        sublist.Insert(new Subscription { Subject = Encoding.UTF8.GetBytes(subject), Queue = queue }).ShouldBeNull();
        sublist.Insert(new Subscription { Subject = Encoding.UTF8.GetBytes(subject), Queue = queue }).ShouldBeNull();

        var result = sublist.Match("$SYS.SERVER.SRV.CLIENT.AUTH.ERR");
        result.QSubs.Count.ShouldBe(1);
        result.QSubs[0].Count.ShouldBe(2);
        result.PSubs.Count.ShouldBe(0);

        var disconnect = new DisconnectEventMsg { Reason = "Authentication Failure" };
        disconnect.Reason.ShouldBe("Authentication Failure");
    }

    [Fact] // T:336
    public void ServerEventsFilteredByTag_ShouldSucceed()
    {
        var tags = new[] { "foo", "bar" };
        MatchesTagFilter(tags, ["foo"]).ShouldBeTrue();
        MatchesTagFilter(tags, ["foo", "bar"]).ShouldBeTrue();
        MatchesTagFilter(tags, ["baz"]).ShouldBeFalse();
        MatchesTagFilter(tags, ["bar"]).ShouldBeTrue();
    }

    [Fact] // T:337
    public void ServerUnstableEventFilterMatch_ShouldSucceed()
    {
        var info = new ServerInfo { Name = "srv10", Cluster = "clust", Host = "127.0.0.1" };

        MatchesServerFilter(info, name: "srv1", exactMatch: true).ShouldBeFalse();
        MatchesServerFilter(info, name: "srv10", exactMatch: true).ShouldBeTrue();
        MatchesServerFilter(info, name: "srv1", exactMatch: false).ShouldBeTrue();
    }

    [Fact] // T:339
    public void ServerEventsStatszSingleServer_ShouldSucceed()
    {
        var server = CreateServer(new ServerOptions { NoSystemAccount = true });
        server.SystemAccount().ShouldBeNull();

        server.SetDefaultSystemAccount().ShouldBeNull();
        var sys = server.SystemAccount();
        sys.ShouldNotBeNull();
        sys!.NumConnections().ShouldBe(0);

        var c = new ClientConnection(ClientKind.Client, server) { Cid = 2001 };
        c.RegisterWithAccount(sys);
        sys.NumConnections().ShouldBe(1);
    }

    [Fact] // T:340
    public void ServerEventsReload_ShouldSucceed()
    {
        var server = CreateServer();
        var resolver = new TrackingResolver();
        SetPrivateField(server, "_accResolver", resolver);

        var before = GetPrivateField<DateTime>(server, "_configTime");
        Thread.Sleep(5);

        server.Reload();

        resolver.ReloadCalls.ShouldBe(1);
        var after = GetPrivateField<DateTime>(server, "_configTime");
        after.ShouldBeGreaterThan(before);
    }

    [Fact] // T:341
    public void ServerEventsLDMKick_ShouldSucceed()
    {
        var server = CreateServer();
        var clients = GetPrivateField<Dictionary<ulong, ClientConnection>>(server, "_clients");

        var c = new ClientConnection(ClientKind.Client, server) { Cid = 999 };
        c.Opts = new ClientOptions { Protocol = ClientProtocol.Info };
        c.Flags |= ClientFlags.FirstPongSent;
        clients[c.Cid] = c;

        server.LDMClientByID(c.Cid).ShouldBeNull();
        server.DisconnectClientByID(c.Cid).ShouldBeNull();
        c.IsClosed().ShouldBeTrue();
        server.DisconnectClientByID(123456).ShouldNotBeNull();
    }

    [Fact] // T:344
    public void ServerEventsProfileZNotBlockingRecvQ_ShouldSucceed()
    {
        var recv = new IpQueue<int>("recvq");
        var priority = new IpQueue<int>("recvqp");

        recv.Push(1).error.ShouldBeNull();
        priority.Push(2).error.ShouldBeNull();

        var (v, ok) = priority.PopOne();
        ok.ShouldBeTrue();
        v.ShouldBe(2);

        recv.Len().ShouldBe(1);
        recv.Pop().ShouldNotBeNull();
    }

    [Fact] // T:348
    public void ServerEventsStatszMaxProcsMemLimit_ShouldSucceed()
    {
        var stats = new ServerStatsMsg
        {
            Server = new ServerInfo { Name = "S", Id = "ID" },
            Stats = new ServerStatsAdvisory
            {
                Start = DateTime.UtcNow,
                MaxProcs = Environment.ProcessorCount * 2,
                MemLimit = 123456789,
            },
        };

        var json = JsonSerializer.Serialize(stats);
        json.ShouldContain("\"gomaxprocs\":");
        json.ShouldContain("\"gomemlimit\":");
        json.ShouldContain("123456789");
    }

    [Fact] // T:349
    public void SubszPagination_ShouldSucceed()
    {
        var sublist = SubscriptionIndex.NewSublistWithCache();

        for (var i = 0; i < 100; i++)
        {
            sublist.Insert(new Subscription
            {
                Subject = Encoding.UTF8.GetBytes($"foo.{i}"),
                Sid = Encoding.UTF8.GetBytes(i.ToString()),
            }).ShouldBeNull();
        }

        var all = new List<Subscription>();
        sublist.All(all);
        all.Count.ShouldBe(100);

        all.Skip(0).Take(10).Count().ShouldBe(10);

        for (var i = 0; i < 10; i++)
        {
            sublist.Insert(new Subscription
            {
                Subject = Encoding.UTF8.GetBytes("bar.*"),
                Sid = Encoding.UTF8.GetBytes($"b{i}"),
            }).ShouldBeNull();
        }

        var bar = sublist.Match("bar.A");
        bar.PSubs.Count.ShouldBe(10);
        bar.PSubs.Skip(0).Take(5).Count().ShouldBe(5);
    }

    [Fact] // T:350
    public void ServerEventsConnectDisconnectForGlobalAcc_ShouldSucceed()
    {
        var server = CreateServer();
        var global = server.GlobalAccount();
        global.ShouldNotBeNull();
        global!.Name.ShouldBe(ServerConstants.DefaultGlobalAccount);

        var connectSubj = string.Format(SystemSubjects.ConnectEventSubj, ServerConstants.DefaultGlobalAccount);
        var disconnectSubj = string.Format(SystemSubjects.DisconnectEventSubj, ServerConstants.DefaultGlobalAccount);
        connectSubj.ShouldBe("$SYS.ACCOUNT.$G.CONNECT");
        disconnectSubj.ShouldBe("$SYS.ACCOUNT.$G.DISCONNECT");

        var c = new ClientConnection(ClientKind.Client, server) { Cid = 3001 };
        c.RegisterWithAccount(global);
        global.NumConnections().ShouldBe(1);

        ((INatsAccount)global).RemoveClient(c);
        global.NumConnections().ShouldBe(0);
    }

    [Fact] // T:333
    public void GatewayNameClientInfo_ShouldSucceed()
        => ServerEventsConnectDisconnectForGlobalAcc_ShouldSucceed();

    [Fact] // T:314
    public void AccountReqMonitoring_ShouldSucceed()
    {
        var acc = Account.NewAccount("ACC-MON");
        var id1 = acc.NextEventId();
        var id2 = acc.NextEventId();

        id1.ShouldNotBeNullOrWhiteSpace();
        id2.ShouldNotBeNullOrWhiteSpace();
        id1.ShouldNotBe(id2);
    }

    [Fact] // T:345
    public void ServerEventsStatsZJetStreamApiLevel_ShouldSucceed()
    {
        var (server, err) = NatsServer.NewServer(new ServerOptions { JetStream = true });
        err.ShouldBeNull();
        server.ShouldNotBeNull();

        JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0);
        server!.GetOpts().JetStream.ShouldBeTrue();
    }

    private static NatsServer CreateServer(ServerOptions? opts = null)
    {
        var (server, err) = NatsServer.NewServer(opts ?? new ServerOptions());
        err.ShouldBeNull();
        server.ShouldNotBeNull();
        return server!;
    }

    private static T GetPrivateField<T>(object target, string name)
    {
        var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
        field.ShouldNotBeNull();
        var value = field!.GetValue(target);
        value.ShouldNotBeNull();
        return (T)value!;
    }

    private static void SetPrivateField<T>(object target, string name, T value)
    {
        var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
        field.ShouldNotBeNull();
        field!.SetValue(target, value);
    }

    private static bool MatchesServerFilter(
        ServerInfo info,
        string? cluster = null,
        string? host = null,
        string? name = null,
        bool exactMatch = false)
    {
        if (!string.IsNullOrEmpty(cluster) &&
            !string.Equals(info.Cluster, cluster, StringComparison.OrdinalIgnoreCase))
            return false;

        if (!string.IsNullOrEmpty(host) &&
            !string.Equals(info.Host, host, StringComparison.OrdinalIgnoreCase))
            return false;

        if (!string.IsNullOrEmpty(name))
        {
            if (exactMatch)
                return string.Equals(info.Name, name, StringComparison.Ordinal);

            return info.Name.Contains(name, StringComparison.Ordinal);
        }

        return true;
    }

    private static bool MatchesTagFilter(IEnumerable<string> serverTags, IEnumerable<string> requestedTags)
    {
        var set = new HashSet<string>(serverTags, StringComparer.OrdinalIgnoreCase);
        foreach (var tag in requestedTags)
        {
            if (!set.Contains(tag))
                return false;
        }
        return true;
    }

    private sealed class TrackingResolver : ResolverDefaultsOps
    {
        public int ReloadCalls { get; private set; }

        public override Task<string> FetchAsync(string name, CancellationToken ct = default)
            => Task.FromException<string>(new InvalidOperationException($"unknown account: {name}"));

        public override void Reload() => ReloadCalls++;
    }
}
