using System.Security.Claims;
using ColaFlow.API.Hubs;
using FluentAssertions;
using Microsoft.AspNetCore.SignalR;
using Moq;
using Xunit;

namespace ColaFlow.IntegrationTests.SignalR;

/// <summary>
/// Security tests for SignalR hubs - verifying multi-tenant isolation and authentication
/// </summary>
public class SignalRSecurityTests
{
    [Fact]
    public void BaseHub_WithoutTenantId_ThrowsUnauthorizedException()
    {
        // Arrange
        var mockContext = new Mock<HubCallerContext>();
        var claims = new[]
        {
            new Claim("sub", Guid.NewGuid().ToString())
            // Missing tenant_id claim
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));

        var hub = new TestBaseHub
        {
            Context = mockContext.Object
        };

        // Act & Assert
        var act = () => hub.GetCurrentTenantIdPublic();
        act.Should().Throw<UnauthorizedAccessException>()
            .WithMessage("Tenant ID not found in token");
    }

    [Fact]
    public void BaseHub_WithoutUserId_ThrowsUnauthorizedException()
    {
        // Arrange
        var mockContext = new Mock<HubCallerContext>();
        var claims = new[]
        {
            new Claim("tenant_id", Guid.NewGuid().ToString())
            // Missing user_id/sub claim
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));

        var hub = new TestBaseHub
        {
            Context = mockContext.Object
        };

        // Act & Assert
        var act = () => hub.GetCurrentUserIdPublic();
        act.Should().Throw<UnauthorizedAccessException>()
            .WithMessage("User ID not found in token");
    }

    [Fact]
    public async Task BaseHub_OnConnectedAsync_WithMissingTenantId_AbortsConnection()
    {
        // Arrange
        var mockContext = new Mock<HubCallerContext>();
        var mockGroups = new Mock<IGroupManager>();
        var userId = Guid.NewGuid();

        var claims = new[]
        {
            new Claim("sub", userId.ToString())
            // Missing tenant_id
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));
        mockContext.Setup(c => c.ConnectionId).Returns("test-connection");

        var hub = new TestBaseHub
        {
            Context = mockContext.Object,
            Groups = mockGroups.Object
        };

        // Act
        await hub.OnConnectedAsync();

        // Assert
        mockContext.Verify(c => c.Abort(), Times.Once);
    }

    [Fact]
    public async Task BaseHub_OnConnectedAsync_WithMissingUserId_AbortsConnection()
    {
        // Arrange
        var mockContext = new Mock<HubCallerContext>();
        var mockGroups = new Mock<IGroupManager>();
        var tenantId = Guid.NewGuid();

        var claims = new[]
        {
            new Claim("tenant_id", tenantId.ToString())
            // Missing user_id/sub
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));
        mockContext.Setup(c => c.ConnectionId).Returns("test-connection");

        var hub = new TestBaseHub
        {
            Context = mockContext.Object,
            Groups = mockGroups.Object
        };

        // Act
        await hub.OnConnectedAsync();

        // Assert
        mockContext.Verify(c => c.Abort(), Times.Once);
    }

    [Fact]
    public void TenantGroupName_ForDifferentTenants_AreDifferent()
    {
        // Arrange
        var tenant1 = Guid.NewGuid();
        var tenant2 = Guid.NewGuid();

        var mockContext = new Mock<HubCallerContext>();
        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity()));

        var hub = new TestBaseHub
        {
            Context = mockContext.Object
        };

        // Act
        var group1 = hub.GetTenantGroupNamePublic(tenant1);
        var group2 = hub.GetTenantGroupNamePublic(tenant2);

        // Assert
        group1.Should().NotBe(group2);
        group1.Should().Be($"tenant-{tenant1}");
        group2.Should().Be($"tenant-{tenant2}");
    }

    [Fact]
    public async Task MultiTenantIsolation_Tenant1User_AutoJoinsOnlyTenant1Group()
    {
        // Arrange
        var tenant1Id = Guid.NewGuid();
        var user1Id = Guid.NewGuid();

        var mockContext = new Mock<HubCallerContext>();
        var mockGroups = new Mock<IGroupManager>();
        var connectionId = "connection-1";

        var claims = new[]
        {
            new Claim("sub", user1Id.ToString()),
            new Claim("tenant_id", tenant1Id.ToString())
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));
        mockContext.Setup(c => c.ConnectionId).Returns(connectionId);

        var hub = new TestBaseHub
        {
            Context = mockContext.Object,
            Groups = mockGroups.Object
        };

        // Act
        await hub.OnConnectedAsync();

        // Assert
        mockGroups.Verify(g => g.AddToGroupAsync(
            connectionId,
            $"tenant-{tenant1Id}",
            default), Times.Once);

        // Verify NOT added to any other tenant group
        mockGroups.Verify(g => g.AddToGroupAsync(
            connectionId,
            It.Is<string>(s => s != $"tenant-{tenant1Id}"),
            default), Times.Never);
    }

    [Fact]
    public void UserIdExtraction_PrefersSubClaim_OverUserIdClaim()
    {
        // Arrange
        var subClaimId = Guid.NewGuid();
        var userIdClaimId = Guid.NewGuid();

        var mockContext = new Mock<HubCallerContext>();
        var claims = new[]
        {
            new Claim("sub", subClaimId.ToString()),
            new Claim("user_id", userIdClaimId.ToString())
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));

        var hub = new TestBaseHub
        {
            Context = mockContext.Object
        };

        // Act
        var extractedUserId = hub.GetCurrentUserIdPublic();

        // Assert
        extractedUserId.Should().Be(subClaimId); // Should prefer 'sub' claim
    }

    [Fact]
    public void InvalidGuidInTenantIdClaim_ThrowsUnauthorizedException()
    {
        // Arrange
        var mockContext = new Mock<HubCallerContext>();
        var claims = new[]
        {
            new Claim("sub", Guid.NewGuid().ToString()),
            new Claim("tenant_id", "not-a-valid-guid")
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));

        var hub = new TestBaseHub
        {
            Context = mockContext.Object
        };

        // Act & Assert
        var act = () => hub.GetCurrentTenantIdPublic();
        act.Should().Throw<UnauthorizedAccessException>()
            .WithMessage("Tenant ID not found in token");
    }

    [Fact]
    public void InvalidGuidInUserIdClaim_ThrowsUnauthorizedException()
    {
        // Arrange
        var mockContext = new Mock<HubCallerContext>();
        var claims = new[]
        {
            new Claim("sub", "invalid-guid-format"),
            new Claim("tenant_id", Guid.NewGuid().ToString())
        };

        mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims)));

        var hub = new TestBaseHub
        {
            Context = mockContext.Object
        };

        // Act & Assert
        var act = () => hub.GetCurrentUserIdPublic();
        act.Should().Throw<UnauthorizedAccessException>()
            .WithMessage("User ID not found in token");
    }

    // Test helper class
    private class TestBaseHub : BaseHub
    {
        public Guid GetCurrentUserIdPublic() => GetCurrentUserId();
        public Guid GetCurrentTenantIdPublic() => GetCurrentTenantId();
        public string GetTenantGroupNamePublic(Guid tenantId) => GetTenantGroupName(tenantId);
    }
}
