// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.AspNetCore.SignalR.Specification.Tests;
using Microsoft.AspNetCore.SignalR.Tests;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Moq;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Xunit;

namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests;

public class RedisHubLifetimeManagerTests : ScaleoutHubLifetimeManagerTests<TestRedisServer>
{
    private TestRedisServer _server;

    public class TestObject
    {
        public string TestProperty { get; set; }
    }

    private RedisHubLifetimeManager<Hub> CreateLifetimeManager(TestRedisServer server, MessagePackHubProtocolOptions messagePackOptions = null, NewtonsoftJsonHubProtocolOptions jsonOptions = null)
    {
        var options = new RedisOptions() { ConnectionFactory = async (t) => await Task.FromResult(new TestConnectionMultiplexer(server)) };
        messagePackOptions = messagePackOptions ?? new MessagePackHubProtocolOptions();
        jsonOptions = jsonOptions ?? new NewtonsoftJsonHubProtocolOptions();
        return new RedisHubLifetimeManager<Hub>(
            NullLogger<RedisHubLifetimeManager<Hub>>.Instance,
            Options.Create(options),
            new DefaultHubProtocolResolver(new IHubProtocol[]
            {
                    new NewtonsoftJsonHubProtocol(Options.Create(jsonOptions)),
                    new MessagePackHubProtocol(Options.Create(messagePackOptions)),
            }, NullLogger<DefaultHubProtocolResolver>.Instance));
    }

    [Fact]
    public async Task CamelCasedJsonIsPreservedAcrossRedisBoundary()
    {
        var server = new TestRedisServer();

        var messagePackOptions = new MessagePackHubProtocolOptions();

        var jsonOptions = new NewtonsoftJsonHubProtocolOptions();
        jsonOptions.PayloadSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

        using (var client1 = new TestClient())
        using (var client2 = new TestClient())
        {
            // The sending manager has serializer settings
            var manager1 = CreateLifetimeManager(server, messagePackOptions, jsonOptions);

            // The receiving one doesn't matter because of how we serialize!
            var manager2 = CreateLifetimeManager(server);

            var connection1 = HubConnectionContextUtils.Create(client1.Connection);
            var connection2 = HubConnectionContextUtils.Create(client2.Connection);

            await manager1.OnConnectedAsync(connection1).DefaultTimeout();
            await manager2.OnConnectedAsync(connection2).DefaultTimeout();

            await manager1.SendAllAsync("Hello", new object[] { new TestObject { TestProperty = "Foo" } });

            var message = Assert.IsType<InvocationMessage>(await client2.ReadAsync().DefaultTimeout());
            Assert.Equal("Hello", message.Target);
            Assert.Collection(
                message.Arguments,
                arg0 =>
                {
                    var dict = Assert.IsType<JObject>(arg0);
                    Assert.Collection(dict.Properties(),
                        prop =>
                        {
                            Assert.Equal("testProperty", prop.Name);
                            Assert.Equal("Foo", prop.Value.Value<string>());
                        });
                });
        }
    }

    [Fact]
    public async Task ErrorFromConnectionFactoryLogsAndAllowsDisconnect()
    {
        var server = new TestRedisServer();

        var testSink = new TestSink();
        var logger = new TestLogger("", testSink, true);
        var mockLoggerFactory = new Mock<ILoggerFactory>();
        mockLoggerFactory
            .Setup(m => m.CreateLogger(It.IsAny<string>()))
            .Returns((string categoryName) => (ILogger)logger);
        var loggerT = mockLoggerFactory.Object.CreateLogger<RedisHubLifetimeManager<Hub>>();

        var manager = new RedisHubLifetimeManager<Hub>(
            loggerT,
            Options.Create(new RedisOptions()
            {
                ConnectionFactory = _ => throw new ApplicationException("throw from connect")
            }),
            new DefaultHubProtocolResolver(new IHubProtocol[]
            {
            }, NullLogger<DefaultHubProtocolResolver>.Instance));

        using (var client = new TestClient())
        {
            var connection = HubConnectionContextUtils.Create(client.Connection);

            var ex = await Assert.ThrowsAsync<ApplicationException>(() => manager.OnConnectedAsync(connection)).DefaultTimeout();
            Assert.Equal("throw from connect", ex.Message);

            await manager.OnDisconnectedAsync(connection).DefaultTimeout();
        }

        var logs = testSink.Writes.ToArray();
        Assert.Single(logs);
        Assert.Equal("Error connecting to Redis.", logs[0].Message);
        Assert.Equal("throw from connect", logs[0].Exception.Message);
    }

    // Smoke test that Debug.Asserts in TestSubscriber aren't hit
    [Fact]
    public async Task PatternGroupAndUser()
    {
        var server = new TestRedisServer();
        using (var client = new TestClient())
        {
            var manager = CreateLifetimeManager(server);

            var connection = HubConnectionContextUtils.Create(client.Connection);
            connection.UserIdentifier = "*";

            await manager.OnConnectedAsync(connection).DefaultTimeout();

            var groupName = "*";

            await manager.AddToGroupAsync(connection.ConnectionId, groupName).DefaultTimeout();
        }
    }

    public override TestRedisServer CreateBackplane()
    {
        return new TestRedisServer();
    }

    public override HubLifetimeManager<Hub> CreateNewHubLifetimeManager()
    {
        _server = new TestRedisServer();
        return CreateLifetimeManager(_server);
    }

    public override HubLifetimeManager<Hub> CreateNewHubLifetimeManager(TestRedisServer backplane)
    {
        return CreateLifetimeManager(backplane);
    }
}
