using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ScadaLink.Commons.Entities.Security;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.ConfigurationDatabase;
using ScadaLink.ConfigurationDatabase.Repositories;
using ScadaLink.Security;

namespace ScadaLink.Security.Tests;

#region WP-6: LdapAuthService Tests

public class LdapAuthServiceTests
{
    private static SecurityOptions CreateOptions(bool useTls = true, bool allowInsecure = false) => new()
    {
        LdapServer = "ldap.example.com",
        LdapPort = 636,
        LdapUseTls = useTls,
        AllowInsecureLdap = allowInsecure,
        LdapSearchBase = "dc=example,dc=com",
        JwtSigningKey = "test-key-that-is-long-enough-for-hmac-sha256-minimum"
    };

    [Fact]
    public async Task AuthenticateAsync_EmptyUsername_ReturnsFailed()
    {
        var service = new LdapAuthService(
            Options.Create(CreateOptions()),
            NullLogger<LdapAuthService>.Instance);

        var result = await service.AuthenticateAsync("", "password");
        Assert.False(result.Success);
        Assert.Contains("Username is required", result.ErrorMessage);
    }

    [Fact]
    public async Task AuthenticateAsync_EmptyPassword_ReturnsFailed()
    {
        var service = new LdapAuthService(
            Options.Create(CreateOptions()),
            NullLogger<LdapAuthService>.Instance);

        var result = await service.AuthenticateAsync("user", "");
        Assert.False(result.Success);
        Assert.Contains("Password is required", result.ErrorMessage);
    }

    [Fact]
    public async Task AuthenticateAsync_InsecureLdapNotAllowed_ReturnsFailed()
    {
        var service = new LdapAuthService(
            Options.Create(CreateOptions(useTls: false, allowInsecure: false)),
            NullLogger<LdapAuthService>.Instance);

        var result = await service.AuthenticateAsync("user", "password");
        Assert.False(result.Success);
        Assert.Contains("Insecure LDAP", result.ErrorMessage);
    }

    [Fact]
    public async Task AuthenticateAsync_ConnectionFailure_ReturnsFailed()
    {
        // Point to a non-existent server — connection should fail
        var options = CreateOptions();
        options.LdapServer = "nonexistent.invalid";
        options.LdapPort = 9999;

        var service = new LdapAuthService(
            Options.Create(options),
            NullLogger<LdapAuthService>.Instance);

        var result = await service.AuthenticateAsync("user", "password");
        Assert.False(result.Success);
    }
}

#endregion

#region WP-7: JwtTokenService Tests

public class JwtTokenServiceTests
{
    private static SecurityOptions CreateOptions() => new()
    {
        JwtSigningKey = "this-is-a-test-signing-key-for-hmac-sha256-must-be-long-enough",
        JwtExpiryMinutes = 15,
        IdleTimeoutMinutes = 30,
        JwtRefreshThresholdMinutes = 5
    };

    private static JwtTokenService CreateService(SecurityOptions? options = null)
    {
        return new JwtTokenService(
            Options.Create(options ?? CreateOptions()),
            NullLogger<JwtTokenService>.Instance);
    }

    [Fact]
    public void GenerateToken_ContainsCorrectClaims()
    {
        var service = CreateService();
        var token = service.GenerateToken(
            "John Doe", "johnd",
            new[] { "Admin", "Design" },
            new[] { "1", "2" });

        var principal = service.ValidateToken(token);
        Assert.NotNull(principal);

        Assert.Equal("John Doe", principal!.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value);
        Assert.Equal("johnd", principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value);

        var roles = principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToList();
        Assert.Contains("Admin", roles);
        Assert.Contains("Design", roles);

        var siteIds = principal.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value).ToList();
        Assert.Contains("1", siteIds);
        Assert.Contains("2", siteIds);

        Assert.NotNull(principal.FindFirst(JwtTokenService.LastActivityClaimType));
    }

    [Fact]
    public void GenerateToken_NullSiteIds_NoSiteIdClaims()
    {
        var service = CreateService();
        var token = service.GenerateToken("User", "user", new[] { "Admin" }, null);
        var principal = service.ValidateToken(token);

        Assert.NotNull(principal);
        Assert.Empty(principal!.FindAll(JwtTokenService.SiteIdClaimType));
    }

    [Fact]
    public void ValidateToken_InvalidToken_ReturnsNull()
    {
        var service = CreateService();
        var result = service.ValidateToken("invalid.token.here");
        Assert.Null(result);
    }

    [Fact]
    public void ValidateToken_WrongKey_ReturnsNull()
    {
        var service1 = CreateService();
        var token = service1.GenerateToken("User", "user", new[] { "Admin" }, null);

        var service2 = CreateService(new SecurityOptions
        {
            JwtSigningKey = "a-completely-different-signing-key-for-hmac-sha256-validation",
            JwtExpiryMinutes = 15,
            IdleTimeoutMinutes = 30,
            JwtRefreshThresholdMinutes = 5
        });
        var result = service2.ValidateToken(token);
        Assert.Null(result);
    }

    [Fact]
    public void ValidateToken_UsesHmacSha256()
    {
        var service = CreateService();
        var token = service.GenerateToken("User", "user", new[] { "Admin" }, null);

        // Decode header to verify algorithm
        var parts = token.Split('.');
        var headerJson = System.Text.Encoding.UTF8.GetString(
            Convert.FromBase64String(parts[0].PadRight((parts[0].Length + 3) & ~3, '=')));
        Assert.Contains("HS256", headerJson);
    }

    [Fact]
    public void ShouldRefresh_TokenNearExpiry_ReturnsTrue()
    {
        var options = CreateOptions();
        options.JwtExpiryMinutes = 3; // Token expires in 3 min, threshold is 5 min
        var service = CreateService(options);

        var token = service.GenerateToken("User", "user", new[] { "Admin" }, null);
        var principal = service.ValidateToken(token);

        Assert.True(service.ShouldRefresh(principal!));
    }

    [Fact]
    public void ShouldRefresh_TokenFarFromExpiry_ReturnsFalse()
    {
        var service = CreateService(); // 15 min expiry, 5 min threshold
        var token = service.GenerateToken("User", "user", new[] { "Admin" }, null);
        var principal = service.ValidateToken(token);

        Assert.False(service.ShouldRefresh(principal!));
    }

    [Fact]
    public void IsIdleTimedOut_RecentActivity_ReturnsFalse()
    {
        var service = CreateService();
        var token = service.GenerateToken("User", "user", new[] { "Admin" }, null);
        var principal = service.ValidateToken(token);

        Assert.False(service.IsIdleTimedOut(principal!));
    }

    [Fact]
    public void IsIdleTimedOut_NoLastActivityClaim_ReturnsTrue()
    {
        var service = CreateService();
        var principal = new ClaimsPrincipal(new ClaimsIdentity(new[]
        {
            new Claim(JwtTokenService.DisplayNameClaimType, "User")
        }));

        Assert.True(service.IsIdleTimedOut(principal));
    }

    [Fact]
    public void RefreshToken_ReturnsNewTokenWithUpdatedClaims()
    {
        var service = CreateService();
        var originalToken = service.GenerateToken("User", "user", new[] { "Admin" }, null);
        var principal = service.ValidateToken(originalToken);

        var newToken = service.RefreshToken(principal!, new[] { "Admin", "Design" }, new[] { "1" });
        Assert.NotNull(newToken);

        var newPrincipal = service.ValidateToken(newToken!);
        var roles = newPrincipal!.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToList();
        Assert.Contains("Design", roles);
    }

    [Fact]
    public void RefreshToken_MissingClaims_ReturnsNull()
    {
        var service = CreateService();
        var principal = new ClaimsPrincipal(new ClaimsIdentity());

        var result = service.RefreshToken(principal, new[] { "Admin" }, null);
        Assert.Null(result);
    }
}

#endregion

#region WP-8: RoleMapper Tests

public class RoleMapperTests : IDisposable
{
    private readonly ScadaLinkDbContext _context;
    private readonly SecurityRepository _securityRepo;
    private readonly RoleMapper _roleMapper;

    public RoleMapperTests()
    {
        var options = new DbContextOptionsBuilder<ScadaLinkDbContext>()
            .UseSqlite("DataSource=:memory:")
            .Options;

        _context = new ScadaLinkDbContext(options);
        _context.Database.OpenConnection();
        _context.Database.EnsureCreated();
        _securityRepo = new SecurityRepository(_context);
        _roleMapper = new RoleMapper(_securityRepo);
    }

    public void Dispose()
    {
        _context.Database.CloseConnection();
        _context.Dispose();
    }

    [Fact]
    public async Task MapGroupsToRoles_MultiRoleExtraction()
    {
        // Add mappings (note: seed data adds SCADA-Admins -> Admin)
        _context.LdapGroupMappings.Add(new LdapGroupMapping("Designers", "Design"));
        _context.LdapGroupMappings.Add(new LdapGroupMapping("Deployers", "Deployment"));
        await _context.SaveChangesAsync();

        var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "SCADA-Admins", "Designers" });

        Assert.Contains("Admin", result.Roles);
        Assert.Contains("Design", result.Roles);
        Assert.DoesNotContain("Deployment", result.Roles);
    }

    [Fact]
    public async Task MapGroupsToRoles_SiteScopedDeployment()
    {
        var site1 = new Site("Site1", "S-001");
        var site2 = new Site("Site2", "S-002");
        _context.Sites.AddRange(site1, site2);
        _context.LdapGroupMappings.Add(new LdapGroupMapping("SiteDeployers", "Deployment"));
        await _context.SaveChangesAsync();

        var mapping = await _context.LdapGroupMappings.SingleAsync(m => m.LdapGroupName == "SiteDeployers");
        _context.SiteScopeRules.AddRange(
            new SiteScopeRule { LdapGroupMappingId = mapping.Id, SiteId = site1.Id },
            new SiteScopeRule { LdapGroupMappingId = mapping.Id, SiteId = site2.Id });
        await _context.SaveChangesAsync();

        var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "SiteDeployers" });

        Assert.Contains("Deployment", result.Roles);
        Assert.False(result.IsSystemWideDeployment);
        Assert.Contains(site1.Id.ToString(), result.PermittedSiteIds);
        Assert.Contains(site2.Id.ToString(), result.PermittedSiteIds);
    }

    [Fact]
    public async Task MapGroupsToRoles_SystemWideDeployment_NoScopeRules()
    {
        _context.LdapGroupMappings.Add(new LdapGroupMapping("GlobalDeployers", "Deployment"));
        await _context.SaveChangesAsync();

        var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "GlobalDeployers" });

        Assert.Contains("Deployment", result.Roles);
        Assert.True(result.IsSystemWideDeployment);
        Assert.Empty(result.PermittedSiteIds);
    }

    [Fact]
    public async Task MapGroupsToRoles_UnrecognizedGroups_Ignored()
    {
        var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "NonExistentGroup", "AnotherRandom" });

        Assert.Empty(result.Roles);
        Assert.Empty(result.PermittedSiteIds);
        Assert.False(result.IsSystemWideDeployment);
    }

    [Fact]
    public async Task MapGroupsToRoles_NoMatchingGroups_NoRoles()
    {
        var result = await _roleMapper.MapGroupsToRolesAsync(Array.Empty<string>());

        Assert.Empty(result.Roles);
    }

    [Fact]
    public async Task MapGroupsToRoles_CaseInsensitiveGroupMatch()
    {
        // "SCADA-Admins" is seeded
        var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "scada-admins" });

        Assert.Contains("Admin", result.Roles);
    }
}

#endregion

#region WP-9: Authorization Policy Tests

public class AuthorizationPolicyTests
{
    [Fact]
    public async Task AdminPolicy_AdminRole_Succeeds()
    {
        var principal = CreatePrincipal(new[] { "Admin" });
        var result = await EvaluatePolicy(AuthorizationPolicies.RequireAdmin, principal);
        Assert.True(result);
    }

    [Fact]
    public async Task AdminPolicy_DesignRole_Fails()
    {
        var principal = CreatePrincipal(new[] { "Design" });
        var result = await EvaluatePolicy(AuthorizationPolicies.RequireAdmin, principal);
        Assert.False(result);
    }

    [Fact]
    public async Task DesignPolicy_DesignRole_Succeeds()
    {
        var principal = CreatePrincipal(new[] { "Design" });
        var result = await EvaluatePolicy(AuthorizationPolicies.RequireDesign, principal);
        Assert.True(result);
    }

    [Fact]
    public async Task DeploymentPolicy_DeploymentRole_Succeeds()
    {
        var principal = CreatePrincipal(new[] { "Deployment" });
        var result = await EvaluatePolicy(AuthorizationPolicies.RequireDeployment, principal);
        Assert.True(result);
    }

    [Fact]
    public async Task NoRoles_DeniedAll()
    {
        var principal = CreatePrincipal(Array.Empty<string>());
        Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireAdmin, principal));
        Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireDesign, principal));
        Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireDeployment, principal));
    }

    [Fact]
    public async Task SiteScope_SystemWideDeployer_Succeeds()
    {
        var handler = new SiteScopeAuthorizationHandler();
        var claims = new List<Claim>
        {
            new(JwtTokenService.RoleClaimType, "Deployment")
            // No SiteId claims = system-wide
        };
        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));

        var requirement = new SiteScopeRequirement("42");
        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);

        await handler.HandleAsync(context);

        Assert.True(context.HasSucceeded);
    }

    [Fact]
    public async Task SiteScope_PermittedSite_Succeeds()
    {
        var handler = new SiteScopeAuthorizationHandler();
        var claims = new List<Claim>
        {
            new(JwtTokenService.RoleClaimType, "Deployment"),
            new(JwtTokenService.SiteIdClaimType, "1"),
            new(JwtTokenService.SiteIdClaimType, "2")
        };
        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));

        var requirement = new SiteScopeRequirement("1");
        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);

        await handler.HandleAsync(context);

        Assert.True(context.HasSucceeded);
    }

    [Fact]
    public async Task SiteScope_UnpermittedSite_Fails()
    {
        var handler = new SiteScopeAuthorizationHandler();
        var claims = new List<Claim>
        {
            new(JwtTokenService.RoleClaimType, "Deployment"),
            new(JwtTokenService.SiteIdClaimType, "1")
        };
        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));

        var requirement = new SiteScopeRequirement("99");
        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);

        await handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
    }

    [Fact]
    public async Task SiteScope_NoDeploymentRole_Fails()
    {
        var handler = new SiteScopeAuthorizationHandler();
        var claims = new List<Claim>
        {
            new(JwtTokenService.RoleClaimType, "Admin")
        };
        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));

        var requirement = new SiteScopeRequirement("1");
        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);

        await handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
    }

    private static ClaimsPrincipal CreatePrincipal(string[] roles, string[]? siteIds = null)
    {
        var claims = new List<Claim>();
        foreach (var role in roles)
            claims.Add(new Claim(JwtTokenService.RoleClaimType, role));
        if (siteIds != null)
            foreach (var siteId in siteIds)
                claims.Add(new Claim(JwtTokenService.SiteIdClaimType, siteId));

        return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
    }

    private static async Task<bool> EvaluatePolicy(string policyName, ClaimsPrincipal principal)
    {
        var services = new ServiceCollection();
        services.AddScadaLinkAuthorization();
        services.AddLogging();

        using var provider = services.BuildServiceProvider();
        var authService = provider.GetRequiredService<IAuthorizationService>();

        var result = await authService.AuthorizeAsync(principal, null, policyName);
        return result.Succeeded;
    }
}

#endregion
