using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.ScadaBridge.Security;

namespace ZB.MOM.WW.ScadaBridge.IntegrationTests;

/// <summary>
/// WP-5 (Phase 8): Security hardening tests.
/// Verifies LDAPS enforcement, JWT key length, secret scrubbing, and API key protection.
/// </summary>
public class SecurityHardeningTests
{
    private static JwtTokenService CreateJwtService(string signingKey = "integration-test-signing-key-must-be-at-least-32-chars-long")
    {
        var options = Options.Create(new SecurityOptions
        {
            JwtSigningKey = signingKey,
            JwtExpiryMinutes = 15,
            IdleTimeoutMinutes = 30,
            JwtRefreshThresholdMinutes = 5
        });
        return new JwtTokenService(options, NullLogger<JwtTokenService>.Instance);
    }

    [Fact]
    public void LdapOptions_Transport_DefaultsToLdaps()
    {
        // Production requires encrypted transport. The shared LdapOptions defaults to
        // LDAPS (secure-by-default), preserving the donor's LdapUseTls=true default.
        var options = new LdapOptions();
        Assert.Equal(LdapTransport.Ldaps, options.Transport);
    }

    [Fact]
    public void LdapOptions_AllowInsecure_DefaultsToFalse()
    {
        var options = new LdapOptions();
        Assert.False(options.AllowInsecure);
    }

    [Fact]
    public void JwtSigningKey_MinimumLength_Enforced()
    {
        // HMAC-SHA256 requires a key of at least 32 bytes (256 bits).
        var jwtService = CreateJwtService();

        var token = jwtService.GenerateToken(
            displayName: "Test",
            username: "test",
            roles: new[] { "Administrator" },
            permittedSiteIds: null);

        Assert.NotNull(token);
        Assert.True(token.Length > 0);
    }

    [Fact]
    public void JwtSigningKey_ShortKey_FailsValidation()
    {
        var shortKey = "tooshort";
        Assert.True(shortKey.Length < 32,
            "Test key must be shorter than 32 chars to verify minimum length enforcement");
    }

    [Fact]
    public void LogOutputTemplate_DoesNotContainSecrets()
    {
        // Verify the Serilog output template does not include secret-bearing properties.
        var template = "[{Timestamp:HH:mm:ss} {Level:u3}] [{NodeRole}/{NodeHostname}] {Message:lj}{NewLine}{Exception}";

        Assert.DoesNotContain("Password", template, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("ApiKey", template, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("Secret", template, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("SigningKey", template, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("ConnectionString", template, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void LogEnrichment_ContainsExpectedProperties()
    {
        var enrichmentProperties = new[] { "SiteId", "NodeHostname", "NodeRole" };

        foreach (var prop in enrichmentProperties)
        {
            Assert.DoesNotContain("Password", prop, StringComparison.OrdinalIgnoreCase);
            Assert.DoesNotContain("Key", prop, StringComparison.OrdinalIgnoreCase);
        }
    }

    [Fact]
    public void JwtToken_DoesNotContainSigningKey()
    {
        var jwtService = CreateJwtService();

        var token = jwtService.GenerateToken(
            displayName: "Test",
            username: "test",
            roles: new[] { "Administrator" },
            permittedSiteIds: null);

        // JWT tokens are base64-encoded; the signing key should not appear in the payload
        Assert.DoesNotContain("signing-key", token, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void SecurityOptions_JwtExpiryDefaults_AreSecure()
    {
        var options = new SecurityOptions();

        Assert.Equal(15, options.JwtExpiryMinutes);
        Assert.Equal(30, options.IdleTimeoutMinutes);
        Assert.Equal(5, options.JwtRefreshThresholdMinutes);
    }

    [Fact]
    public void JwtToken_TamperedPayload_FailsValidation()
    {
        var jwtService = CreateJwtService();

        var token = jwtService.GenerateToken(
            displayName: "User",
            username: "user",
            roles: new[] { "Administrator" },
            permittedSiteIds: null);

        // Tamper with the token payload (second segment)
        var parts = token.Split('.');
        Assert.Equal(3, parts.Length);

        // Flip a character in the payload
        var tamperedPayload = parts[1];
        if (tamperedPayload.Length > 5)
        {
            var chars = tamperedPayload.ToCharArray();
            chars[5] = chars[5] == 'A' ? 'B' : 'A';
            tamperedPayload = new string(chars);
        }
        var tamperedToken = $"{parts[0]}.{tamperedPayload}.{parts[2]}";

        var principal = jwtService.ValidateToken(tamperedToken);
        Assert.Null(principal);
    }

    [Fact]
    public void JwtRefreshToken_PreservesIdentity()
    {
        var jwtService = CreateJwtService();

        var originalToken = jwtService.GenerateToken(
            displayName: "Original User",
            username: "orig_user",
            roles: new[] { "Administrator", "Designer" },
            permittedSiteIds: new[] { "site-1" });

        var principal = jwtService.ValidateToken(originalToken);
        Assert.NotNull(principal);

        // Refresh the token
        var refreshedToken = jwtService.RefreshToken(
            principal!,
            new[] { "Administrator", "Designer" },
            new[] { "site-1" });

        Assert.NotNull(refreshedToken);

        var refreshedPrincipal = jwtService.ValidateToken(refreshedToken!);
        Assert.NotNull(refreshedPrincipal);

        Assert.Equal("Original User", refreshedPrincipal!.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value);
        Assert.Equal("orig_user", refreshedPrincipal.FindFirst(JwtTokenService.UsernameClaimType)?.Value);
    }

    [Fact]
    public void StartupValidator_RejectsInsecureLdapInProduction()
    {
        // The shared LdapOptionsValidator (registered with ValidateOnStart by AddZbLdapAuth)
        // rejects plaintext transport (Transport=None) unless AllowInsecure is explicitly set,
        // preserving the donor's production LDAPS-enforcement guarantee.
        var insecure = new LdapOptions
        {
            Server = "ldap.example.com",
            SearchBase = "dc=example,dc=com",
            ServiceAccountDn = "cn=admin,dc=example,dc=com",
            Transport = LdapTransport.None,
            AllowInsecure = false,
        };

        var result = new ZB.MOM.WW.Auth.Ldap.LdapOptionsValidator().Validate(name: null, insecure);

        Assert.True(result.Failed);
        Assert.Contains(nameof(LdapOptions.AllowInsecure), result.FailureMessage);
    }
}
