using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;

namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Authorization;

[Trait("Category", "Unit")]
public sealed class TriePermissionEvaluatorTests
{
    private static readonly DateTime Now = new(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc);
    private readonly FakeTimeProvider _time = new();

    private sealed class FakeTimeProvider : TimeProvider
    {
        public DateTime Utc { get; set; } = Now;
        public override DateTimeOffset GetUtcNow() => new(Utc, TimeSpan.Zero);
    }

    private static NodeAcl Row(string group, NodeAclScopeKind scope, string? scopeId, NodePermissions flags) =>
        new()
        {
            NodeAclRowId = Guid.NewGuid(),
            NodeAclId = $"acl-{Guid.NewGuid():N}",
            GenerationId = 1,
            ClusterId = "c1",
            LdapGroup = group,
            ScopeKind = scope,
            ScopeId = scopeId,
            PermissionFlags = flags,
        };

    private static UserAuthorizationState Session(string[] groups, DateTime? resolvedUtc = null, string clusterId = "c1") =>
        new()
        {
            SessionId = "sess",
            ClusterId = clusterId,
            LdapGroups = groups,
            MembershipResolvedUtc = resolvedUtc ?? Now,
            AuthGenerationId = 1,
            MembershipVersion = 1,
        };

    private static NodeScope Scope(string cluster = "c1") =>
        new()
        {
            ClusterId = cluster,
            NamespaceId = "ns",
            UnsAreaId = "area",
            UnsLineId = "line",
            EquipmentId = "eq",
            TagId = "tag",
            Kind = NodeHierarchyKind.Equipment,
        };

    private TriePermissionEvaluator MakeEvaluator(NodeAcl[] rows)
    {
        var cache = new PermissionTrieCache();
        cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
        return new TriePermissionEvaluator(cache, _time);
    }

    [Fact]
    public void Allow_When_RequiredFlag_Matched()
    {
        var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read)]);

        var decision = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.Read, Scope());

        decision.Verdict.ShouldBe(AuthorizationVerdict.Allow);
        decision.Provenance.Count.ShouldBe(1);
    }

    [Fact]
    public void NotGranted_When_NoMatchingGroup()
    {
        var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read)]);

        var decision = evaluator.Authorize(Session(["cn=unrelated"]), OpcUaOperation.Read, Scope());

        decision.Verdict.ShouldBe(AuthorizationVerdict.NotGranted);
        decision.Provenance.ShouldBeEmpty();
    }

    [Fact]
    public void NotGranted_When_FlagsInsufficient()
    {
        var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read)]);

        var decision = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.WriteOperate, Scope());

        decision.Verdict.ShouldBe(AuthorizationVerdict.NotGranted);
    }

    [Fact]
    public void HistoryRead_Requires_Its_Own_Bit()
    {
        // User has Read but not HistoryRead
        var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read)]);

        var liveRead = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.Read, Scope());
        var historyRead = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.HistoryRead, Scope());

        liveRead.IsAllowed.ShouldBeTrue();
        historyRead.IsAllowed.ShouldBeFalse("HistoryRead uses its own NodePermissions flag, not Read");
    }

    [Fact]
    public void CrossCluster_Session_Denied()
    {
        var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read)]);
        var otherSession = Session(["cn=ops"], clusterId: "c-other");

        var decision = evaluator.Authorize(otherSession, OpcUaOperation.Read, Scope(cluster: "c1"));

        decision.Verdict.ShouldBe(AuthorizationVerdict.NotGranted);
    }

    [Fact]
    public void StaleSession_FailsClosed()
    {
        var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read)]);
        var session = Session(["cn=ops"], resolvedUtc: Now);
        _time.Utc = Now.AddMinutes(10); // well past the 5-min AuthCacheMaxStaleness default

        var decision = evaluator.Authorize(session, OpcUaOperation.Read, Scope());

        decision.Verdict.ShouldBe(AuthorizationVerdict.NotGranted);
    }

    [Fact]
    public void NoCachedTrie_ForCluster_Denied()
    {
        var cache = new PermissionTrieCache();  // empty cache
        var evaluator = new TriePermissionEvaluator(cache, _time);

        var decision = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.Read, Scope());

        decision.Verdict.ShouldBe(AuthorizationVerdict.NotGranted);
    }

    [Fact]
    public void OperationToPermission_Mapping_IsTotal()
    {
        foreach (var op in Enum.GetValues<OpcUaOperation>())
        {
            // Must not throw — every OpcUaOperation needs a mapping or the compliance-check
            // "every operation wired" fails.
            TriePermissionEvaluator.MapOperationToPermission(op);
        }
    }
}
