using System.Security.Claims;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Security.Jwt;

namespace ZB.MOM.WW.OtOpcUa.Security.Tests;

public sealed class JwtTokenServiceTests
{
    private const string TestKey = "this-is-a-32-byte-test-signing-key!!";

    private static JwtTokenService NewService(string key = TestKey, int expiryMinutes = 15) =>
        new(Options.Create(new JwtOptions
        {
            SigningKey = key,
            Issuer = "otopcua-test",
            Audience = "otopcua-test",
            ExpiryMinutes = expiryMinutes,
        }), NullLogger<JwtTokenService>.Instance);

    /// <summary>Verifies that a short signing key throws InvalidOperationException.</summary>
    [Fact]
    public void Short_signing_key_throws()
    {
        Should.Throw<InvalidOperationException>(() => NewService("too-short"));
    }

    /// <summary>Verifies that issued tokens validate and preserve claims.</summary>
    [Fact]
    public void Issue_then_validate_roundtrips_claims()
    {
        var jwt = NewService();
        var token = jwt.Issue("Joe User", "joe", new[] { "ReadOnly", "AlarmAck" });

        jwt.TryValidate(token, out var principal).ShouldBeTrue();
        principal.ShouldNotBeNull();
        principal!.FindFirst(JwtTokenService.UsernameClaimType)!.Value.ShouldBe("joe");
        principal.FindFirst(JwtTokenService.DisplayNameClaimType)!.Value.ShouldBe("Joe User");
        principal.FindAll(JwtTokenService.RoleClaimType)
            .Select(c => c.Value).ShouldBe(new[] { "ReadOnly", "AlarmAck" }, ignoreOrder: true);
    }

    /// <summary>Verifies that tampered tokens are rejected during validation.</summary>
    [Fact]
    public void Tampered_token_is_rejected()
    {
        var jwt = NewService();
        var token = jwt.Issue("Joe", "joe", Array.Empty<string>());

        // Corrupt the payload — flip a char in the middle segment.
        var parts = token.Split('.');
        parts[1] = parts[1][..^2] + "AA";
        var tampered = string.Join('.', parts);

        jwt.TryValidate(tampered, out var principal).ShouldBeFalse();
        principal.ShouldBeNull();
    }

    /// <summary>Verifies that expired tokens are rejected during validation.</summary>
    [Fact]
    public void Expired_token_is_rejected()
    {
        // Issue with -1 min expiry so it's already past at validate time.
        var jwt = NewService(expiryMinutes: -1);
        var token = jwt.Issue("Joe", "joe", Array.Empty<string>());

        jwt.TryValidate(token, out var principal).ShouldBeFalse();
        principal.ShouldBeNull();
    }

    /// <summary>Verifies that tokens signed with a different key are rejected.</summary>
    [Fact]
    public void Cross_key_token_is_rejected()
    {
        var issuer = NewService();
        var token = issuer.Issue("Joe", "joe", Array.Empty<string>());

        var different = NewService("a-different-32-byte-signing-key!!!");
        different.TryValidate(token, out var principal).ShouldBeFalse();
        principal.ShouldBeNull();
    }
}
