using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Api.Security;
using Tiku.Application.Security;

namespace Tiku.IntegrationTests.Api;

public sealed class AuthorizationManifestTests
{
    private const int ExpectedActionCount = 399;
    private const string ExpectedSha256 = "e4460d18dbd88cb8a4293c650688f03ddaa67e69a423e70d6675c635e237c316";

    [Fact]
    public void Controller_authorization_surface_matches_reviewed_manifest()
    {
        var descriptors = typeof(Tiku.Api.ApiProgramMarker).Assembly.GetTypes()
            .Where(type => !type.IsAbstract && typeof(ControllerBase).IsAssignableFrom(type))
            .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
                .Where(method => method.GetCustomAttributes<HttpMethodAttribute>().Any())
                .Select(method => Describe(type, method)))
            .OrderBy(value => value, StringComparer.Ordinal)
            .ToArray();
        var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join('\n', descriptors))))
            .ToLowerInvariant();

        Assert.True(
            descriptors.Length == ExpectedActionCount && hash == ExpectedSha256,
            $"Authorization manifest changed. count={descriptors.Length}, sha256={hash}");
    }

    [Fact]
    public async Task Runtime_controller_endpoints_have_authorization_and_audit_metadata()
    {
        await using var factory = new ApiTestFactory();
        using var client = factory.CreateClient();
        _ = await client.GetAsync("/api/health");
        var endpoints = factory.Services.GetRequiredService<EndpointDataSource>().Endpoints
            .Where(endpoint => endpoint.Metadata.GetMetadata<ControllerActionDescriptor>() is not null)
            .ToArray();

        Assert.NotEmpty(endpoints);
        foreach (var endpoint in endpoints)
        {
            var anonymous = endpoint.Metadata.GetMetadata<IAllowAnonymous>() is not null;
            var metadata = endpoint.Metadata.GetMetadata<EndpointAuthorizationMetadata>();
            if (anonymous)
            {
                Assert.Null(metadata);
                continue;
            }

            Assert.NotNull(metadata);
            Assert.False(string.IsNullOrWhiteSpace(metadata.AuditAction));
            Assert.Contains(metadata.Realm, new[] { "authenticated", "tenant", "platform" });
            if (metadata.Realm == "tenant" &&
                metadata.Module is { } module &&
                PermissionModuleCatalog.RequiredFeatures.TryGetValue(module, out var requiredFeature) &&
                requiredFeature is not null)
            {
                Assert.Contains(requiredFeature, metadata.RequiredFeatures);
            }
        }
    }

    [Theory]
    [InlineData("questions", SaasFeatureCatalog.PrivateQuestionBank)]
    [InlineData("vocabulary", SaasFeatureCatalog.Vocabulary)]
    [InlineData("handbook", SaasFeatureCatalog.Handbook)]
    [InlineData("scoreline", SaasFeatureCatalog.Scoreline)]
    [InlineData("videos", SaasFeatureCatalog.Video)]
    public void Content_import_route_maps_to_an_explicit_feature(string importType, string expectedFeature)
    {
        Assert.Equal(expectedFeature, SaasFeatureCatalog.ResolveContentImportFeature(importType));
    }

    [Fact]
    public void Tenant_content_permissions_are_split_by_purchasable_feature()
    {
        var expected = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            [BackendPermissions.TenantContentManage] = SaasFeatureCatalog.PrivateQuestionBank,
            [BackendPermissions.TenantVocabularyManage] = SaasFeatureCatalog.Vocabulary,
            [BackendPermissions.TenantHandbookManage] = SaasFeatureCatalog.Handbook,
            [BackendPermissions.TenantVideoManage] = SaasFeatureCatalog.Video,
            [BackendPermissions.TenantScorelineManage] = SaasFeatureCatalog.Scoreline,
            [BackendPermissions.TenantSiteContentManage] = SaasFeatureCatalog.SiteContent
        };

        foreach (var pair in expected)
        {
            var module = PermissionModuleCatalog.ResolvePermissionModuleCode(pair.Key);
            Assert.Equal(pair.Value, PermissionModuleCatalog.RequiredFeatures[module]);
        }
    }

    private static string Describe(Type controller, MethodInfo action)
    {
        var controllerRoute = controller.GetCustomAttribute<RouteAttribute>()?.Template ?? string.Empty;
        var http = action.GetCustomAttributes<HttpMethodAttribute>().ToArray();
        var methods = string.Join(',', http.SelectMany(attribute => attribute.HttpMethods).Distinct().Order(StringComparer.Ordinal));
        var templates = string.Join(',', http.Select(attribute => attribute.Template ?? string.Empty).Distinct().Order(StringComparer.Ordinal));
        var policies = controller.GetCustomAttributes<AuthorizeAttribute>()
            .Concat(action.GetCustomAttributes<AuthorizeAttribute>())
            .Select(attribute => attribute.Policy ?? "authenticated")
            .Order(StringComparer.Ordinal);
        var anonymous = controller.IsDefined(typeof(AllowAnonymousAttribute)) ||
                        action.IsDefined(typeof(AllowAnonymousAttribute));
        return $"{methods}|{controllerRoute}/{templates}|{controller.Name}.{action.Name}|anonymous={anonymous}|policies={string.Join(',', policies)}";
    }
}
