using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using eagle;
using Grpc.Core;
using NUnit.Framework;
using Timer = System.Timers.Timer;

namespace eagle0.Tests {
    [TestFixture]
    public class PersistentClientConnectionLifecycleTests {
        [Test]
        public void DisposeStopsAndPreventsAllConnectionTimers() {
            var connection =
                    new PersistentClientConnection(null, new Metadata(), CancellationToken.None);
            SetPrivateField(connection, "_retryTimer", new Timer());
            SetPrivateField(connection, "_idleCheckTimer", new Timer());
            SetPrivateField(connection, "_heartbeatTimer", new Timer());
            connection.OnChannelDead = () => {};
            connection.OnCommandError =
                    _ => {};

            connection.Dispose();

            Assert.IsNull(GetPrivateField<Timer>(connection, "_retryTimer"));
            Assert.IsNull(GetPrivateField<Timer>(connection, "_idleCheckTimer"));
            Assert.IsNull(GetPrivateField<Timer>(connection, "_heartbeatTimer"));
            Assert.IsNull(connection.OnChannelDead);
            Assert.IsNull(connection.OnCommandError);

            InvokePrivate(connection, "ScheduleReconnect", "test-after-dispose");
            InvokePrivate(connection, "StartIdleCheckTimer");
            InvokePrivate(connection, "StartHeartbeatTimer");
            connection.ForceReconnect();

            Assert.IsNull(GetPrivateField<Timer>(connection, "_retryTimer"));
            Assert.IsNull(GetPrivateField<Timer>(connection, "_idleCheckTimer"));
            Assert.IsNull(GetPrivateField<Timer>(connection, "_heartbeatTimer"));
            Assert.DoesNotThrow(connection.Dispose);
        }

        [Test]
        public async Task UnauthenticatedFailureRequiresLoginWhenTokenRefreshFails() {
            var connection = new PersistentClientConnection(
                    null,
                    new Metadata(),
                    CancellationToken.None,
                    () => Task.FromResult(false));

            await InvokePrivateAsync(
                    connection,
                    "HandleUnauthenticatedAsync",
                    new RpcException(new Status(StatusCode.Unauthenticated, "Token expired")));

            Assert.AreEqual(ConnectionState.AuthenticationRequired, connection.CurrentState);
            Assert.IsNull(connection.NextReconnectAttempt);
            Assert.AreEqual(
                    ConnectionCircuitBreaker.State.Closed,
                    connection.CircuitBreaker.CurrentState);

            connection.Dispose();
        }

        private static T GetPrivateField<T>(object target, string fieldName) {
            var field = target.GetType().GetField(
                    fieldName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(field, $"Missing private field '{fieldName}'");
            return (T)field.GetValue(target);
        }

        private static void SetPrivateField<T>(object target, string fieldName, T value) {
            var field = target.GetType().GetField(
                    fieldName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(field, $"Missing private field '{fieldName}'");
            field.SetValue(target, value);
        }

        private static void InvokePrivate(object target, string methodName, params object[] args) {
            var method = target.GetType().GetMethod(
                    methodName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(method, $"Missing private method '{methodName}'");
            method.Invoke(target, args);
        }

        private static async Task
        InvokePrivateAsync(object target, string methodName, params object[] args) {
            var method = target.GetType().GetMethod(
                    methodName,
                    BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.NotNull(method, $"Missing private method '{methodName}'");
            await(Task) method.Invoke(target, args);
        }
    }
}
