using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;

/// <summary>
/// Tests for <see cref="DashboardGroupRoleMapper"/>, the shared-Auth
/// <see cref="IGroupRoleMapper{TRole}"/> seam over the dashboard's
/// LDAP-group → role mapping. Behaviour must match the existing
/// <c>DashboardAuthenticator.MapGroupsToRoles</c> precedence/case rules.
/// </summary>
public sealed class DashboardGroupRoleMapperTests
{
    private static DashboardGroupRoleMapper CreateMapper(Dictionary<string, string> mapping)
    {
        GatewayOptions options = new()
        {
            Dashboard = new DashboardOptions
            {
                GroupToRole = mapping,
            },
        };

        return new DashboardGroupRoleMapper(Options.Create(options));
    }

    private static Dictionary<string, string> StandardMapping() => new(StringComparer.OrdinalIgnoreCase)
    {
        ["GwAdmin"] = DashboardRoles.Admin,
        ["GwReader"] = DashboardRoles.Viewer,
    };

    /// <summary>Verifies full-DN match, leading-RDN fallback, case-insensitivity, and unmapped → empty.</summary>
    /// <param name="ldapGroup">The LDAP group name or distinguished name.</param>
    /// <param name="expectedRole">The expected role or null if no match.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Theory]
    [InlineData("GwAdmin", DashboardRoles.Admin)]
    [InlineData("gwadmin", DashboardRoles.Admin)]
    [InlineData("ou=GwAdmin,ou=groups,dc=zb,dc=local", DashboardRoles.Admin)]
    [InlineData("OtherGroup", null)]
    public async Task MapAsync_ResolvesByShortNameAndDistinguishedName(
        string ldapGroup,
        string? expectedRole)
    {
        DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());

        GroupRoleMapping<string> result = await mapper.MapAsync([ldapGroup], CancellationToken.None);

        if (expectedRole is null)
        {
            Assert.Empty(result.Roles);
        }
        else
        {
            Assert.Equal(expectedRole, Assert.Single(result.Roles));
        }

        Assert.Null(result.Scope);
    }

    /// <summary>Verifies that admin and viewer roles are both emitted when both groups are present.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task MapAsync_AdminPlusViewer_BothRolesEmitted()
    {
        DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());

        GroupRoleMapping<string> result = await mapper.MapAsync(
            ["GwAdmin", "GwReader"],
            CancellationToken.None);

        Assert.Contains(DashboardRoles.Admin, result.Roles);
        Assert.Contains(DashboardRoles.Viewer, result.Roles);
    }

    /// <summary>Verifies that an empty GroupToRole map yields no roles.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task MapAsync_EmptyMapping_ReturnsNoRoles()
    {
        DashboardGroupRoleMapper mapper = CreateMapper(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));

        GroupRoleMapping<string> result = await mapper.MapAsync(["GwAdmin"], CancellationToken.None);

        Assert.Empty(result.Roles);
    }

    /// <summary>
    /// Canonical roles: an LDAP user in the admin group must resolve
    /// to the canonical role value <c>"Administrator"</c> (not the legacy
    /// <c>"Admin"</c>), and the reader group to <c>"Viewer"</c>. Asserted with
    /// string LITERALS — independent of <see cref="DashboardRoles"/> — so a
    /// regression on the constant's value is caught here.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task MapAsync_AdminGroup_ResolvesToCanonicalAdministratorValue()
    {
        DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());

        GroupRoleMapping<string> adminResult = await mapper.MapAsync(["GwAdmin"], CancellationToken.None);
        GroupRoleMapping<string> readerResult = await mapper.MapAsync(["GwReader"], CancellationToken.None);

        Assert.Equal("Administrator", Assert.Single(adminResult.Roles));
        Assert.Equal("Viewer", Assert.Single(readerResult.Roles));
    }

    /// <summary>
    /// The canonical admin value (<c>"Administrator"</c>) passes the
    /// admin-only gate while a <c>"Viewer"</c> fails it — asserted with literals,
    /// proving enforcement is bound to the new value and the legacy <c>"Admin"</c>
    /// string is no longer what authorizes admin actions.
    /// </summary>
    [Fact]
    public void AdminOnly_AuthorizesCanonicalAdministratorButNotViewer()
    {
        IReadOnlyList<string> adminGate = DashboardAuthorizationRequirement.AdminOnly.RequiredRoles;

        Assert.Contains("Administrator", adminGate);
        Assert.DoesNotContain("Admin", adminGate);
        Assert.DoesNotContain("Viewer", adminGate);
    }

    /// <summary>
    /// Verifies the extracted shared helper is the single source of truth: it
    /// produces the same roles the mapper does for the same inputs.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SharedHelper_MatchesMapperOutput()
    {
        Dictionary<string, string> mapping = StandardMapping();
        DashboardGroupRoleMapper mapper = CreateMapper(mapping);
        string[] groups = ["ou=GwAdmin,ou=groups,dc=zb,dc=local", "gwreader"];

        IReadOnlyList<string> helperRoles = DashboardGroupRoleMapping.MapGroupsToRoles(groups, mapping);
        GroupRoleMapping<string> mapperResult = await mapper.MapAsync(groups, CancellationToken.None);

        Assert.Equal([.. helperRoles.OrderBy(r => r)], [.. mapperResult.Roles.OrderBy(r => r)]);
    }
}
