using ColaFlow.API.Hubs;
using ColaFlow.API.Services;
using ColaFlow.API.Tests.Helpers;
using FluentAssertions;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;

namespace ColaFlow.API.Tests.Services;

public class RealtimeNotificationServiceTests
{
    private readonly Mock<IHubContext<ProjectHub>> _mockProjectHubContext;
    private readonly Mock<IHubContext<NotificationHub>> _mockNotificationHubContext;
    private readonly Mock<ILogger<RealtimeNotificationService>> _mockLogger;
    private readonly Mock<IHubClients> _mockProjectClients;
    private readonly Mock<IHubClients> _mockNotificationClients;
    private readonly Mock<IClientProxy> _mockClientProxy;
    private readonly RealtimeNotificationService _service;

    public RealtimeNotificationServiceTests()
    {
        _mockProjectHubContext = new Mock<IHubContext<ProjectHub>>();
        _mockNotificationHubContext = new Mock<IHubContext<NotificationHub>>();
        _mockLogger = new Mock<ILogger<RealtimeNotificationService>>();
        _mockProjectClients = new Mock<IHubClients>();
        _mockNotificationClients = new Mock<IHubClients>();
        _mockClientProxy = new Mock<IClientProxy>();

        _mockProjectHubContext.Setup(h => h.Clients).Returns(_mockProjectClients.Object);
        _mockNotificationHubContext.Setup(h => h.Clients).Returns(_mockNotificationClients.Object);
        _mockProjectClients.Setup(c => c.Group(It.IsAny<string>())).Returns(_mockClientProxy.Object);
        _mockNotificationClients.Setup(c => c.Group(It.IsAny<string>())).Returns(_mockClientProxy.Object);
        _mockNotificationClients.Setup(c => c.User(It.IsAny<string>())).Returns(_mockClientProxy.Object);

        _service = new RealtimeNotificationService(
            _mockProjectHubContext.Object,
            _mockNotificationHubContext.Object,
            _mockLogger.Object);
    }

    [Fact]
    public async Task NotifyProjectCreated_SendsToTenantGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var project = new { Id = projectId, Name = "Test Project" };

        // Act
        await _service.NotifyProjectCreated(tenantId, projectId, project);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "ProjectCreated",
            It.Is<object[]>(args => args.Length == 1 && args[0] == project),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyProjectUpdated_SendsToBothProjectAndTenantGroups()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var project = new { Id = projectId, Name = "Updated Project" };

        // Act
        await _service.NotifyProjectUpdated(tenantId, projectId, project);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockProjectClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "ProjectUpdated",
            It.Is<object[]>(args => args.Length == 1 && args[0] == project),
            default), Times.Exactly(2));
    }

    [Fact]
    public async Task NotifyProjectArchived_SendsToBothGroups()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();

        // Act
        await _service.NotifyProjectArchived(tenantId, projectId);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockProjectClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "ProjectArchived",
            It.Is<object[]>(args => args.Length == 1),
            default), Times.Exactly(2));
    }

    [Fact]
    public async Task NotifyProjectArchived_ContainsCorrectProjectId()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        object? capturedData = null;

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("ProjectArchived", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                capturedData = args[0];
            })
            .Returns(Task.CompletedTask);

        // Act
        await _service.NotifyProjectArchived(tenantId, projectId);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "ProjectId").Should().Be(projectId);
    }

    [Fact]
    public async Task NotifyProjectUpdate_SendsToProjectGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var updateData = new { Field = "Status", Value = "Active" };

        // Act
        await _service.NotifyProjectUpdate(tenantId, projectId, updateData);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "ProjectUpdated",
            It.Is<object[]>(args => args.Length == 1 && args[0] == updateData),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyIssueCreated_SendsToProjectGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var issue = new { Id = Guid.NewGuid(), Title = "New Issue" };

        // Act
        await _service.NotifyIssueCreated(tenantId, projectId, issue);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "IssueCreated",
            It.Is<object[]>(args => args.Length == 1 && args[0] == issue),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyIssueUpdated_SendsToProjectGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var issue = new { Id = Guid.NewGuid(), Title = "Updated Issue" };

        // Act
        await _service.NotifyIssueUpdated(tenantId, projectId, issue);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "IssueUpdated",
            It.Is<object[]>(args => args.Length == 1 && args[0] == issue),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyIssueDeleted_SendsToProjectGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var issueId = Guid.NewGuid();

        // Act
        await _service.NotifyIssueDeleted(tenantId, projectId, issueId);

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "IssueDeleted",
            It.Is<object[]>(args => args.Length == 1),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyIssueDeleted_ContainsCorrectIssueId()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var issueId = Guid.NewGuid();
        object? capturedData = null;

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("IssueDeleted", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                capturedData = args[0];
            })
            .Returns(Task.CompletedTask);

        // Act
        await _service.NotifyIssueDeleted(tenantId, projectId, issueId);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "IssueId").Should().Be(issueId);
    }

    [Fact]
    public async Task NotifyIssueStatusChanged_SendsWithCorrectData()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var issueId = Guid.NewGuid();
        var oldStatus = "To Do";
        var newStatus = "In Progress";
        object? capturedData = null;

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("IssueStatusChanged", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                capturedData = args[0];
            })
            .Returns(Task.CompletedTask);

        // Act
        await _service.NotifyIssueStatusChanged(tenantId, projectId, issueId, oldStatus, newStatus);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "IssueId").Should().Be(issueId);
        TestHelpers.GetPropertyValue<string>(capturedData!, "OldStatus").Should().Be(oldStatus);
        TestHelpers.GetPropertyValue<string>(capturedData!, "NewStatus").Should().Be(newStatus);
        TestHelpers.GetPropertyValue<DateTime>(capturedData!, "ChangedAt").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public async Task NotifyIssueStatusChanged_SendsToProjectGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var issueId = Guid.NewGuid();

        // Act
        await _service.NotifyIssueStatusChanged(tenantId, projectId, issueId, "To Do", "Done");

        // Assert
        _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "IssueStatusChanged",
            It.IsAny<object[]>(),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyUser_SendsToSpecificUser()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var message = "Test notification";
        var type = "info";

        // Act
        await _service.NotifyUser(userId, message, type);

        // Assert
        _mockNotificationClients.Verify(c => c.User(userId.ToString()), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "Notification",
            It.Is<object[]>(args => args.Length == 1),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyUser_ContainsCorrectMessageData()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var message = "Important update";
        var type = "warning";
        object? capturedData = null;

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("Notification", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                capturedData = args[0];
            })
            .Returns(Task.CompletedTask);

        // Act
        await _service.NotifyUser(userId, message, type);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<string>(capturedData!, "Message").Should().Be(message);
        TestHelpers.GetPropertyValue<string>(capturedData!, "Type").Should().Be(type);
        TestHelpers.GetPropertyValue<DateTime>(capturedData!, "Timestamp").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public async Task NotifyUser_WithDefaultType_UsesInfo()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var message = "Test";
        object? capturedData = null;

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("Notification", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                capturedData = args[0];
            })
            .Returns(Task.CompletedTask);

        // Act
        await _service.NotifyUser(userId, message);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<string>(capturedData!, "Type").Should().Be("info");
    }

    [Fact]
    public async Task NotifyUsersInTenant_SendsToTenantGroup()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var message = "Tenant announcement";
        var type = "info";

        // Act
        await _service.NotifyUsersInTenant(tenantId, message, type);

        // Assert
        _mockNotificationClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "Notification",
            It.Is<object[]>(args => args.Length == 1),
            default), Times.Once);
    }

    [Fact]
    public async Task NotifyUsersInTenant_ContainsCorrectData()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var message = "System maintenance scheduled";
        var type = "warning";
        object? capturedData = null;

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("Notification", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                capturedData = args[0];
            })
            .Returns(Task.CompletedTask);

        // Act
        await _service.NotifyUsersInTenant(tenantId, message, type);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<string>(capturedData!, "Message").Should().Be(message);
        TestHelpers.GetPropertyValue<string>(capturedData!, "Type").Should().Be(type);
        TestHelpers.GetPropertyValue<DateTime>(capturedData!, "Timestamp").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public async Task MultipleNotifications_AllSentCorrectly()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var projectId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        // Act
        await _service.NotifyProjectCreated(tenantId, projectId, new { Name = "Project1" });
        await _service.NotifyUser(userId, "Hello");
        await _service.NotifyUsersInTenant(tenantId, "Announcement");

        // Assert
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            It.IsAny<string>(),
            It.IsAny<object[]>(),
            default), Times.Exactly(3));
    }
}
