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

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

[Trait("Category", "Unit")]
public sealed class PermissionTrieTests
{
    private static NodeAcl Row(string group, NodeAclScopeKind scope, string? scopeId, NodePermissions flags, string clusterId = "c1") =>
        new()
        {
            NodeAclRowId = Guid.NewGuid(),
            NodeAclId = $"acl-{Guid.NewGuid():N}",
            ClusterId = clusterId,
            LdapGroup = group,
            ScopeKind = scope,
            ScopeId = scopeId,
            PermissionFlags = flags,
        };

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

    /// <summary>Verifies cluster-level grant cascades to every tag.</summary>
    [Fact]
    public void ClusterLevelGrant_Cascades_ToEveryTag()
    {
        var rows = new[] { Row("cn=ops", NodeAclScopeKind.Cluster, scopeId: null, NodePermissions.Read) };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows);

        var matches = trie.CollectMatches(
            EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"),
            ["cn=ops"]);

        matches.Count.ShouldBe(1);
        matches[0].PermissionFlags.ShouldBe(NodePermissions.Read);
        matches[0].Scope.ShouldBe(NodeAclScopeKind.Cluster);
    }

    /// <summary>Verifies equipment-scope grant does not leak to sibling.</summary>
    [Fact]
    public void EquipmentScope_DoesNotLeak_ToSibling()
    {
        var paths = new Dictionary<string, NodeAclPath>(StringComparer.OrdinalIgnoreCase)
        {
            ["eq-A"] = new(new[] { "ns", "area1", "line1", "eq-A" }),
        };
        var rows = new[] { Row("cn=ops", NodeAclScopeKind.Equipment, "eq-A", NodePermissions.Read) };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths);

        var matchA = trie.CollectMatches(EquipmentTag("c1", "ns", "area1", "line1", "eq-A", "tag1"), ["cn=ops"]);
        var matchB = trie.CollectMatches(EquipmentTag("c1", "ns", "area1", "line1", "eq-B", "tag1"), ["cn=ops"]);

        matchA.Count.ShouldBe(1);
        matchB.ShouldBeEmpty("grant at eq-A must not apply to sibling eq-B");
    }

    /// <summary>Verifies multiple groups union their permission flags.</summary>
    [Fact]
    public void MultiGroup_Union_OrsPermissionFlags()
    {
        var rows = new[]
        {
            Row("cn=readers", NodeAclScopeKind.Cluster, null, NodePermissions.Read),
            Row("cn=writers", NodeAclScopeKind.Cluster, null, NodePermissions.WriteOperate),
        };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows);

        var matches = trie.CollectMatches(
            EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"),
            ["cn=readers", "cn=writers"]);

        matches.Count.ShouldBe(2);
        var combined = matches.Aggregate(NodePermissions.None, (acc, m) => acc | m.PermissionFlags);
        combined.ShouldBe(NodePermissions.Read | NodePermissions.WriteOperate);
    }

    /// <summary>Verifies no matching group returns empty.</summary>
    [Fact]
    public void NoMatchingGroup_ReturnsEmpty()
    {
        var rows = new[] { Row("cn=different", NodeAclScopeKind.Cluster, null, NodePermissions.Read) };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows);

        var matches = trie.CollectMatches(
            EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"),
            ["cn=ops"]);

        matches.ShouldBeEmpty();
    }

    /// <summary>
    ///     Galaxy is now a standard Equipment-kind driver: Galaxy points are ordinary equipment
    ///     tags, so a grant on a Galaxy equipment resolves via the Equipment walk and must not leak
    ///     to a sibling Galaxy equipment.
    /// </summary>
    [Fact]
    public void Galaxy_EquipmentScope_Grant_DoesNotLeak_To_Sibling_Equipment()
    {
        var paths = new Dictionary<string, NodeAclPath>(StringComparer.OrdinalIgnoreCase)
        {
            ["gal-eq-A"] = new(new[] { "ns-gal", "area1", "line1", "gal-eq-A" }),
        };
        var rows = new[] { Row("cn=ops", NodeAclScopeKind.Equipment, "gal-eq-A", NodePermissions.Read) };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths);

        var matchA = trie.CollectMatches(EquipmentTag("c1", "ns-gal", "area1", "line1", "gal-eq-A", "tag1"), ["cn=ops"]);
        var matchB = trie.CollectMatches(EquipmentTag("c1", "ns-gal", "area1", "line1", "gal-eq-B", "tag1"), ["cn=ops"]);

        matchA.Count.ShouldBe(1);
        matchB.ShouldBeEmpty("grant at gal-eq-A must not apply to sibling gal-eq-B");
    }

    /// <summary>
    ///     A grant on a Galaxy equipment tag is reported with its true Equipment-walk structural
    ///     scope (Equipment), not a Galaxy-specific scope — Galaxy now flows through the same
    ///     Equipment permission walk as every other equipment tag.
    /// </summary>
    [Fact]
    public void Galaxy_EquipmentScope_Grant_Reports_Equipment_Scope()
    {
        var paths = new Dictionary<string, NodeAclPath>(StringComparer.OrdinalIgnoreCase)
        {
            ["gal-eq"] = new(new[] { "ns-gal", "area1", "line1", "gal-eq" }),
        };
        var rows = new[] { Row("cn=ops", NodeAclScopeKind.Equipment, "gal-eq", NodePermissions.Read) };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths);

        var matches = trie.CollectMatches(EquipmentTag("c1", "ns-gal", "area1", "line1", "gal-eq", "tag1"), ["cn=ops"]);

        matches.Count.ShouldBe(1);
        matches[0].Scope.ShouldBe(NodeAclScopeKind.Equipment,
            "Galaxy equipment grants resolve via the Equipment walk and report Equipment scope");
    }

    /// <summary>Verifies cross-cluster grant does not leak.</summary>
    [Fact]
    public void CrossCluster_Grant_DoesNotLeak()
    {
        var rows = new[] { Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, clusterId: "c-other") };
        var trie = PermissionTrieBuilder.Build("c1", 1, rows);

        var matches = trie.CollectMatches(
            EquipmentTag("c1", "ns", "area1", "line1", "eq1", "tag1"),
            ["cn=ops"]);

        matches.ShouldBeEmpty("rows for cluster c-other must not land in c1's trie");
    }

    /// <summary>Verifies build is idempotent.</summary>
    [Fact]
    public void Build_IsIdempotent()
    {
        var rows = new[]
        {
            Row("cn=a", NodeAclScopeKind.Cluster, null, NodePermissions.Read),
            Row("cn=b", NodeAclScopeKind.Cluster, null, NodePermissions.WriteOperate),
        };

        var trie1 = PermissionTrieBuilder.Build("c1", 1, rows);
        var trie2 = PermissionTrieBuilder.Build("c1", 1, rows);

        trie1.Root.Grants.Count.ShouldBe(trie2.Root.Grants.Count);
        trie1.ClusterId.ShouldBe(trie2.ClusterId);
        trie1.GenerationId.ShouldBe(trie2.GenerationId);
    }
}
