using System.Security.Claims;
using ColaFlow.API.Hubs;
using ColaFlow.API.Tests.Helpers;
using ColaFlow.Modules.ProjectManagement.Application.Services;
using FluentAssertions;
using Microsoft.AspNetCore.SignalR;
using Moq;

namespace ColaFlow.API.Tests.Hubs;

public class ProjectHubTests
{
    private readonly Mock<IHubCallerClients> _mockClients;
    private readonly Mock<IGroupManager> _mockGroups;
    private readonly Mock<HubCallerContext> _mockContext;
    private readonly Mock<IClientProxy> _mockClientProxy;
    private readonly Mock<IProjectPermissionService> _mockPermissionService;
    private readonly ProjectHub _hub;
    private readonly Guid _userId;
    private readonly Guid _tenantId;
    private const string ConnectionId = "test-connection-id";

    public ProjectHubTests()
    {
        _mockClients = new Mock<IHubCallerClients>();
        _mockGroups = new Mock<IGroupManager>();
        _mockContext = new Mock<HubCallerContext>();
        _mockClientProxy = new Mock<IClientProxy>();
        _mockPermissionService = new Mock<IProjectPermissionService>();

        _userId = Guid.NewGuid();
        _tenantId = Guid.NewGuid();

        // Setup context with valid claims
        var claims = new[]
        {
            new Claim("sub", _userId.ToString()),
            new Claim("tenant_id", _tenantId.ToString())
        };

        _mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));
        _mockContext.Setup(c => c.ConnectionId).Returns(ConnectionId);
        _mockContext.Setup(c => c.ConnectionAborted).Returns(CancellationToken.None);

        // Setup clients mock
        _mockClients.Setup(c => c.OthersInGroup(It.IsAny<string>())).Returns(_mockClientProxy.Object);

        // Default: allow all permissions (individual tests can override)
        _mockPermissionService
            .Setup(s => s.IsUserProjectMemberAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(true);

        _hub = new ProjectHub(_mockPermissionService.Object)
        {
            Clients = _mockClients.Object,
            Groups = _mockGroups.Object,
            Context = _mockContext.Object
        };
    }

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

        // Act
        await _hub.JoinProject(projectId);

        // Assert
        _mockGroups.Verify(g => g.AddToGroupAsync(
            ConnectionId,
            $"project-{projectId}",
            default), Times.Once);
    }

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

        // Act
        await _hub.JoinProject(projectId);

        // Assert
        _mockClients.Verify(c => c.OthersInGroup($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "UserJoinedProject",
            It.Is<object[]>(args =>
                args.Length == 1 &&
                args[0] != null),
            default), Times.Once);
    }

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

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

        // Act
        await _hub.JoinProject(projectId);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "UserId").Should().Be(_userId);
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "ProjectId").Should().Be(projectId);
        TestHelpers.GetPropertyValue<DateTime>(capturedData!, "JoinedAt").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public async Task JoinProject_MultipleProjects_AddsToMultipleGroups()
    {
        // Arrange
        var projectId1 = Guid.NewGuid();
        var projectId2 = Guid.NewGuid();

        // Act
        await _hub.JoinProject(projectId1);
        await _hub.JoinProject(projectId2);

        // Assert
        _mockGroups.Verify(g => g.AddToGroupAsync(
            ConnectionId,
            $"project-{projectId1}",
            default), Times.Once);
        _mockGroups.Verify(g => g.AddToGroupAsync(
            ConnectionId,
            $"project-{projectId2}",
            default), Times.Once);
    }

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

        // Act
        await _hub.LeaveProject(projectId);

        // Assert
        _mockGroups.Verify(g => g.RemoveFromGroupAsync(
            ConnectionId,
            $"project-{projectId}",
            default), Times.Once);
    }

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

        // Act
        await _hub.LeaveProject(projectId);

        // Assert
        _mockClients.Verify(c => c.OthersInGroup($"project-{projectId}"), Times.Once);
        _mockClientProxy.Verify(p => p.SendCoreAsync(
            "UserLeftProject",
            It.Is<object[]>(args =>
                args.Length == 1 &&
                args[0] != null),
            default), Times.Once);
    }

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

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

        // Act
        await _hub.LeaveProject(projectId);

        // Assert
        capturedData.Should().NotBeNull();
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "UserId").Should().Be(_userId);
        TestHelpers.GetPropertyValue<Guid>(capturedData!, "ProjectId").Should().Be(projectId);
        TestHelpers.GetPropertyValue<DateTime>(capturedData!, "LeftAt").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

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

        // Act
        await _hub.SendTypingIndicator(projectId, issueId, isTyping: true);

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

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

        // Act
        await _hub.SendTypingIndicator(projectId, issueId, isTyping: false);

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

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

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

        // Act
        await _hub.SendTypingIndicator(projectId, issueId, isTyping);

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

    [Fact]
    public async Task SendTypingIndicator_WithDifferentIssues_BroadcastsSeparately()
    {
        // Arrange
        var projectId = Guid.NewGuid();
        var issueId1 = Guid.NewGuid();
        var issueId2 = Guid.NewGuid();

        // Act
        await _hub.SendTypingIndicator(projectId, issueId1, isTyping: true);
        await _hub.SendTypingIndicator(projectId, issueId2, isTyping: true);

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

    [Fact]
    public async Task SendTypingIndicator_ToggleTyping_SendsBothStates()
    {
        // Arrange
        var projectId = Guid.NewGuid();
        var issueId = Guid.NewGuid();
        var capturedStates = new List<bool>();

        _mockClientProxy
            .Setup(p => p.SendCoreAsync("TypingIndicator", It.IsAny<object[]>(), default))
            .Callback<string, object[], CancellationToken>((method, args, ct) =>
            {
                var isTyping = TestHelpers.GetPropertyValue<bool>(args[0], "IsTyping");
                capturedStates.Add(isTyping);
            })
            .Returns(Task.CompletedTask);

        // Act
        await _hub.SendTypingIndicator(projectId, issueId, isTyping: true);
        await _hub.SendTypingIndicator(projectId, issueId, isTyping: false);

        // Assert
        capturedStates.Should().HaveCount(2);
        capturedStates[0].Should().BeTrue();
        capturedStates[1].Should().BeFalse();
    }

    [Fact]
    public async Task JoinProject_WithoutPermission_ThrowsHubException()
    {
        // Arrange
        var projectId = Guid.NewGuid();
        _mockPermissionService
            .Setup(s => s.IsUserProjectMemberAsync(_userId, projectId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(false);

        // Act & Assert
        var act = async () => await _hub.JoinProject(projectId);
        await act.Should().ThrowAsync<HubException>()
            .WithMessage("You do not have permission to access this project");
    }

    [Fact]
    public async Task JoinProject_WithoutPermission_DoesNotAddToGroup()
    {
        // Arrange
        var projectId = Guid.NewGuid();
        _mockPermissionService
            .Setup(s => s.IsUserProjectMemberAsync(_userId, projectId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(false);

        // Act
        try
        {
            await _hub.JoinProject(projectId);
        }
        catch (HubException)
        {
            // Expected exception
        }

        // Assert
        _mockGroups.Verify(g => g.AddToGroupAsync(
            It.IsAny<string>(),
            It.IsAny<string>(),
            default), Times.Never);
    }

    [Fact]
    public async Task LeaveProject_WithoutPermission_ThrowsHubException()
    {
        // Arrange
        var projectId = Guid.NewGuid();
        _mockPermissionService
            .Setup(s => s.IsUserProjectMemberAsync(_userId, projectId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(false);

        // Act & Assert
        var act = async () => await _hub.LeaveProject(projectId);
        await act.Should().ThrowAsync<HubException>()
            .WithMessage("You do not have permission to access this project");
    }

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

        // Act
        await _hub.JoinProject(projectId);

        // Assert
        _mockPermissionService.Verify(s => s.IsUserProjectMemberAsync(
            _userId,
            projectId,
            It.IsAny<CancellationToken>()), Times.Once);
    }
}
