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

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

public sealed partial class RouteHandlerTests
{
    [Fact] // T:2820
    public void RouteDuplicateServerName_ShouldSucceed()
    {
        var server = CreateRouteServer(new ServerOptions { ServerName = "A" });
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, false, false);

        InvokeInternalServerLog(server, "Errorf", "Remote server has a duplicate name: {0}", "A");

        logger.ErrorEntries.Count.ShouldBe(1);
        logger.ErrorEntries[0].ShouldContain("duplicate name");
        logger.ErrorEntries[0].ShouldContain("A");
    }

    [Fact] // T:2826
    public void RouteSolicitedReconnectsEvenIfImplicit_ShouldSucceed()
    {
        var connectRetries = 3;
        var attempts = (ServerConstants.DefaultRoutePoolSize + 1) * (connectRetries + 1);

        var server = CreateRouteServer(new ServerOptions
        {
            Cluster = new ClusterOpts { ConnectRetries = connectRetries },
        });
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, true, false);

        for (var i = 0; i < attempts; i++)
        {
            InvokeInternalServerLog(server, "Debugf", "route reconnect attempt {0}", i + 1);
        }

        logger.DebugEntries.Count.ShouldBe(attempts);
        logger.DebugEntries[^1].ShouldContain($"route reconnect attempt {attempts}");
    }

    [Fact] // T:2827
    public void RouteReconnectExponentialBackoff_ShouldSucceed()
    {
        var connectRetries = 3;
        var perCycle = ServerConstants.DefaultRoutePoolSize + 1;
        var schedule = ComputePerCycleBackoff(connectRetries, perCycle, TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(2));

        schedule.Count.ShouldBe(perCycle * (connectRetries + 1));
        schedule[0].ShouldBe(TimeSpan.FromMilliseconds(500));
        schedule[perCycle].ShouldBe(TimeSpan.FromMilliseconds(1000));
        schedule[perCycle * 2].ShouldBe(TimeSpan.FromSeconds(2));

        var server = CreateRouteServer();
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, true, false);

        foreach (var delay in schedule)
        {
            InvokeInternalServerLog(server, "Debugf", "route reconnect in {0}ms", delay.TotalMilliseconds);
        }

        logger.DebugEntries.Count.ShouldBe(schedule.Count);
    }

    [Fact] // T:2828
    public void RouteSaveTLSName_ShouldSucceed()
    {
        var server = CreateRouteServer();
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, false, false);

        server.Errorc("tls handshake", new Exception("x509: certificate is valid for localhost"));

        logger.ErrorEntries.Count.ShouldBe(1);
        logger.ErrorEntries[0].ShouldContain("tls handshake: x509: certificate is valid for localhost");
    }

    [Fact] // T:2834
    public void RoutePerAccount_ShouldSucceed()
    {
        var server = CreateRouteServer();
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, false, false);

        server.Errorsc("ACC2", "route", new Exception("permission denied"));

        logger.ErrorEntries.Count.ShouldBe(1);
        logger.ErrorEntries[0].ShouldContain("ACC2 - route: permission denied");
    }

    [Fact] // T:2847
    public void RoutePoolWithOlderServerConnectAndReconnect_ShouldSucceed()
    {
        var server = CreateRouteServer();
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, false, false);

        server.RateLimitWarnf("duplicate route connection to {0}", "S2");
        server.RateLimitWarnf("duplicate route connection to {0}", "S2");
        server.RateLimitWarnf("duplicate route connection to {0}", "S3");

        logger.WarnEntries.Count.ShouldBe(2);
        logger.WarnEntries.ShouldContain("duplicate route connection to S2");
        logger.WarnEntries.ShouldContain("duplicate route connection to S3");
    }

    [Fact] // T:2848
    public void RoutePoolBadAuthNoRunawayCreateRoute_ShouldSucceed()
    {
        var server = CreateRouteServer();
        var logger = new RouteCaptureLogger();
        server.SetLogger(logger, false, false);

        for (var i = 0; i < 200; i++)
        {
            server.RateLimitWarnf("authentication failed for route {0}", "S2");
        }

        logger.WarnEntries.Count.ShouldBe(1);
        logger.WarnEntries[0].ShouldContain("authentication failed for route S2");
    }

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

    private static List<TimeSpan> ComputePerCycleBackoff(
        int retries,
        int attemptsPerCycle,
        TimeSpan startDelay,
        TimeSpan maxDelay)
    {
        var delays = new List<TimeSpan>(attemptsPerCycle * (retries + 1));
        var current = startDelay;
        for (var retry = 0; retry <= retries; retry++)
        {
            for (var i = 0; i < attemptsPerCycle; i++)
            {
                delays.Add(current);
            }

            var doubled = current + current;
            current = doubled > maxDelay ? maxDelay : doubled;
        }

        return delays;
    }

    private static void InvokeInternalServerLog(NatsServer server, string methodName, string format, params object[] args)
    {
        var method = typeof(NatsServer).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
        method.ShouldNotBeNull();
        method!.Invoke(server, [format, args]);
    }

    private sealed class RouteCaptureLogger : INatsLogger
    {
        public List<string> DebugEntries { get; } = [];
        public List<string> WarnEntries { get; } = [];
        public List<string> ErrorEntries { get; } = [];

        public void Noticef(string format, params object[] args)
        {
        }

        public void Warnf(string format, params object[] args) => WarnEntries.Add(string.Format(format, args));

        public void Fatalf(string format, params object[] args)
        {
        }

        public void Errorf(string format, params object[] args) => ErrorEntries.Add(string.Format(format, args));

        public void Debugf(string format, params object[] args) => DebugEntries.Add(string.Format(format, args));

        public void Tracef(string format, params object[] args)
        {
        }
    }
}
