using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Configuration.Services;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;

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

/// <summary>
///     Task 1.7 — control-plane admin roles are standardized on the canonical six
///     (<c>Viewer / Operator / Engineer / Designer / Deployer / Administrator</c>). OtOpcUa
///     uses four of them: ConfigViewer→Viewer, ConfigEditor→Designer, FleetAdmin→Administrator,
///     and the appsettings-only DriverOperator→Operator. These tests pin the canonical role
///     VALUES end-to-end (mapper output claims + the real registered authorization policies) and
///     prove enforcement semantics are preserved (whoever could deploy/administer/operate before
///     still can — it is a rename, not a permission change).
/// </summary>
public sealed class CanonicalAdminRolesTests
{
    // --- (a) the mapper mints the CANONICAL role claim for each native group ----------------

    [Theory]
    [InlineData("Viewer")]        // was ConfigViewer
    [InlineData("Designer")]      // was ConfigEditor
    [InlineData("Administrator")] // was FleetAdmin
    [InlineData("Operator")]      // was DriverOperator (appsettings-only string role)
    public async Task Mapper_yields_canonical_role_for_native_group(string canonicalRole)
    {
        // appsettings GroupToRole baseline carries the canonical value verbatim.
        var mapper = BuildMapper(new Dictionary<string, string> { ["the-group"] = canonicalRole });

        var result = await mapper.MapAsync(["the-group"], CancellationToken.None);

        result.Roles.ShouldContain(canonicalRole);
    }

    [Theory]
    [InlineData(AdminRole.Viewer, "Viewer")]
    [InlineData(AdminRole.Designer, "Designer")]
    [InlineData(AdminRole.Administrator, "Administrator")]
    public async Task System_wide_db_row_role_renders_as_canonical_string(AdminRole role, string expected)
    {
        // The DB path stringifies the enum member name (row.Role.ToString()); renaming the enum
        // members is what makes the persisted/emitted string canonical.
        var mapper = BuildMapper(
            new Dictionary<string, string>(),
            new LdapGroupRoleMapping { LdapGroup = "g", Role = role, IsSystemWide = true });

        var result = await mapper.MapAsync(["g"], CancellationToken.None);

        result.Roles.ShouldContain(expected);
    }

    // --- (b)/(c) the REAL registered authorization policies enforce on the canonical values ---

    [Fact]
    public async Task Deployments_role_check_authorizes_Designer_and_Administrator()
    {
        // Deployments.razor uses [Authorize(Roles="Administrator,Designer")] — a direct role-string
        // check (not a named policy). Reproduce it via RequireRole and prove both still pass.
        var policy = new AuthorizationPolicyBuilder()
            .RequireRole("Administrator", "Designer")
            .Build();
        var authz = BuildAuthorizationService();

        (await authz.AuthorizeAsync(UserInRole("Designer"), policy)).Succeeded.ShouldBeTrue();
        (await authz.AuthorizeAsync(UserInRole("Administrator"), policy)).Succeeded.ShouldBeTrue();
        (await authz.AuthorizeAsync(UserInRole("Viewer"), policy)).Succeeded.ShouldBeFalse();
    }

    [Fact]
    public async Task FleetAdmin_policy_authorizes_only_Administrator()
    {
        var authz = BuildAuthorizationService();

        // RoleGrants.razor is gated by the "FleetAdmin" named policy → RequireRole("Administrator").
        (await authz.AuthorizeAsync(UserInRole("Administrator"), "FleetAdmin")).Succeeded.ShouldBeTrue();
        (await authz.AuthorizeAsync(UserInRole("Designer"), "FleetAdmin")).Succeeded.ShouldBeFalse();
        (await authz.AuthorizeAsync(UserInRole("Operator"), "FleetAdmin")).Succeeded.ShouldBeFalse();
        (await authz.AuthorizeAsync(UserInRole("Viewer"), "FleetAdmin")).Succeeded.ShouldBeFalse();
    }

    [Fact]
    public async Task DriverOperator_policy_authorizes_Operator_and_Administrator()
    {
        var authz = BuildAuthorizationService();

        // DriverStatusPanel/pickers gate on the "DriverOperator" named policy →
        // RequireRole("Operator","Administrator"). Operator (was DriverOperator) and Administrator
        // (was FleetAdmin) both pass; a plain Viewer does not.
        (await authz.AuthorizeAsync(UserInRole("Operator"), "DriverOperator")).Succeeded.ShouldBeTrue();
        (await authz.AuthorizeAsync(UserInRole("Administrator"), "DriverOperator")).Succeeded.ShouldBeTrue();
        (await authz.AuthorizeAsync(UserInRole("Viewer"), "DriverOperator")).Succeeded.ShouldBeFalse();
    }

    // --- helpers ----------------------------------------------------------------------------

    private static ClaimsPrincipal UserInRole(string role)
    {
        // ZbClaimTypes.Role aliases ClaimTypes.Role, the default role-claim type, so RequireRole /
        // IsInRole resolve against it.
        var identity = new ClaimsIdentity(
            [new Claim(ZbClaimTypes.Role, role)], authenticationType: "Test");
        return new ClaimsPrincipal(identity);
    }

    private static IAuthorizationService BuildAuthorizationService()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
        // Use the REAL policy registrations from AddOtOpcUaAuth; it needs the ConfigDbContext for
        // DataProtection key persistence, so register an in-memory one.
        services.AddDbContextFactory<OtOpcUaConfigDbContext>(o => o.UseInMemoryDatabase("authz-test"));
        services.AddDbContext<OtOpcUaConfigDbContext>(o => o.UseInMemoryDatabase("authz-test"));
        services.AddOtOpcUaAuth(new ConfigurationBuilder().Build());

        return services.BuildServiceProvider().GetRequiredService<IAuthorizationService>();
    }

    private static OtOpcUaGroupRoleMapper BuildMapper(
        IDictionary<string, string> groupToRole,
        params LdapGroupRoleMapping[] dbRows)
    {
        var options = Microsoft.Extensions.Options.Options.Create(new LdapOptions
        {
            GroupToRole = new Dictionary<string, string>(groupToRole, StringComparer.OrdinalIgnoreCase),
        });
        return new OtOpcUaGroupRoleMapper(options, new FakeMappingService(dbRows));
    }

    private sealed class FakeMappingService(IReadOnlyList<LdapGroupRoleMapping> rows) : ILdapGroupRoleMappingService
    {
        public Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
            IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
            => Task.FromResult(rows);

        public Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
            => Task.FromResult(rows);

        public Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
            => throw new NotSupportedException();

        public Task DeleteAsync(Guid id, CancellationToken cancellationToken)
            => throw new NotSupportedException();
    }
}
