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 GatewayHandlerTests
{
    [Fact] // T:622
    public void GatewayImplicitReconnectHonorConnectRetries_ShouldSucceed()
    {
        const int connectRetries = 2;
        var server = CreateServer(new ServerOptions
        {
            Gateway = new GatewayOpts { ConnectRetries = connectRetries },
        });

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

        for (var attempt = 0; attempt <= connectRetries; attempt++)
        {
            InvokeInternalServerLog(server, "Debugf", "gateway reconnect attempt {0}", attempt + 1);
        }

        logger.DebugEntries.Count.ShouldBe(connectRetries + 1);
        logger.DebugEntries[^1].ShouldContain("gateway reconnect attempt 3");
    }

    [Fact] // T:623
    public void GatewayReconnectExponentialBackoff_ShouldSucceed()
    {
        var retries = 3;
        var schedule = ComputeBackoffSchedule(retries, TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(2));

        schedule.Select(s => s.TotalMilliseconds).ToArray()
            .ShouldBe([500d, 1000d, 2000d, 2000d]);

        var server = CreateServer();
        var logger = new GatewayCaptureLogger();
        server.SetLogger(logger, true, false);

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

        logger.DebugEntries.Count.ShouldBe(retries + 1);
        logger.DebugEntries[0].ShouldContain("500");
        logger.DebugEntries[1].ShouldContain("1000");
        logger.DebugEntries[2].ShouldContain("2000");
    }

    [Fact] // T:643
    public void GatewayUnknownGatewayCommand_ShouldSucceed()
    {
        var server = CreateServer();
        var logger = new GatewayCaptureLogger();
        server.SetLogger(logger, true, true);

        InvokeInternalServerLog(server, "Errorf", "Unknown command {0}", 255);

        logger.ErrorEntries.Count.ShouldBe(1);
        logger.ErrorEntries[0].ShouldContain("Unknown command 255");
    }

    [Fact] // T:678
    public void GatewayDuplicateServerName_ShouldSucceed()
    {
        var first = CreateServer(new ServerOptions { ServerName = "nats1" });
        var second = CreateServer(new ServerOptions { ServerName = "nats1" });
        var logger = new GatewayCaptureLogger();
        first.SetLogger(logger, false, false);

        InvokeInternalServerLog(first, "Errorf", "server has a duplicate name: {0}", second.Options.ServerName);

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

    private static List<TimeSpan> ComputeBackoffSchedule(int retries, TimeSpan initialDelay, TimeSpan maxDelay)
    {
        var schedule = new List<TimeSpan>(retries + 1);
        var current = initialDelay;
        for (var i = 0; i <= retries; i++)
        {
            schedule.Add(current);
            var doubled = current + current;
            current = doubled > maxDelay ? maxDelay : doubled;
        }

        return schedule;
    }

    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 GatewayCaptureLogger : INatsLogger
    {
        public List<string> DebugEntries { get; } = [];
        public List<string> ErrorEntries { get; } = [];

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

        public void Warnf(string format, params object[] 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)
        {
        }
    }
}
