using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using ROLAC.API.Authorization;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Permissions;
using ROLAC.API.Entities;
using ROLAC.API.Services;
using Xunit;

namespace ROLAC.API.Tests.Services;

public class PermissionServiceTests
{
    // -----------------------------------------------------------------------
    // Harness: a real PermissionService backed by an in-memory EF database.
    // -----------------------------------------------------------------------

    private sealed class Harness
    {
        public required ServiceProvider Provider { get; init; }
        public required PermissionService Service { get; init; }

        public async Task SeedRoleAsync(string roleName, params RolePermission[] permissions)
        {
            using var scope = Provider.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

            var role = new AppRole { Id = $"role-{roleName}", Name = roleName, NormalizedName = roleName.ToUpperInvariant() };
            db.Roles.Add(role);
            foreach (var permission in permissions)
            {
                permission.RoleId = role.Id;
                db.RolePermissions.Add(permission);
            }
            await db.SaveChangesAsync();
        }
    }

    private static Harness BuildHarness()
    {
        var dbName = Guid.NewGuid().ToString();
        var services = new ServiceCollection();
        services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase(dbName));
        var provider = services.BuildServiceProvider();

        var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
        var cache = new MemoryCache(new MemoryCacheOptions());
        return new Harness
        {
            Provider = provider,
            Service  = new PermissionService(scopeFactory, cache,
                new ROLAC.API.Services.Logging.SystemLogQueue(),
                new Microsoft.AspNetCore.Http.HttpContextAccessor()),
        };
    }

    private static RolePermission Perm(string module, bool r = false, bool w = false, bool d = false, bool a = false)
        => new() { Module = module, CanRead = r, CanWrite = w, CanDelete = d, CanApprove = a };

    // -----------------------------------------------------------------------
    // HasPermissionAsync
    // -----------------------------------------------------------------------

    [Fact]
    public async Task HasPermission_RoleGrantsAction_ReturnsTrue()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true, w: true));

        Assert.True(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Read));
        Assert.True(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Write));
    }

    [Fact]
    public async Task HasPermission_RoleLacksAction_ReturnsFalse()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true)); // read only

        Assert.False(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Delete));
        Assert.False(await h.Service.HasPermissionAsync(["finance"], Modules.Members, PermissionActions.Read));
    }

    [Fact]
    public async Task HasPermission_UnionAcrossRoles_ReturnsTrueIfAnyRoleGrants()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("pastor",  Perm(Modules.Members, r: true));
        await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true, w: true));

        // User holds both roles — should get the union.
        Assert.True(await h.Service.HasPermissionAsync(["pastor", "finance"], Modules.Members, PermissionActions.Read));
        Assert.True(await h.Service.HasPermissionAsync(["pastor", "finance"], Modules.Givings, PermissionActions.Write));
        Assert.False(await h.Service.HasPermissionAsync(["pastor", "finance"], Modules.Members, PermissionActions.Delete));
    }

    // -----------------------------------------------------------------------
    // GetEffectivePermissionsAsync
    // -----------------------------------------------------------------------

    [Fact]
    public async Task GetEffectivePermissions_SuperAdmin_ReturnsAllModulesFull()
    {
        var h = BuildHarness(); // no rows seeded at all

        var effective = await h.Service.GetEffectivePermissionsAsync(["super_admin"]);

        Assert.Equal(Modules.All.Count, effective.Count);
        foreach (var module in Modules.All)
        {
            Assert.True(effective[module].Read);
            Assert.True(effective[module].Write);
            Assert.True(effective[module].Delete);
            Assert.True(effective[module].Approve);
        }
    }

    [Fact]
    public async Task GetEffectivePermissions_MergesFlagsAcrossRoles()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("a", Perm(Modules.Expenses, r: true));
        await h.SeedRoleAsync("b", Perm(Modules.Expenses, w: true, a: true));

        var effective = await h.Service.GetEffectivePermissionsAsync(["a", "b"]);

        Assert.True(effective[Modules.Expenses].Read);
        Assert.True(effective[Modules.Expenses].Write);
        Assert.True(effective[Modules.Expenses].Approve);
        Assert.False(effective[Modules.Expenses].Delete);
    }

    [Fact]
    public async Task GetEffectivePermissions_OmitsModulesWithNoGrant()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("member"); // role exists but no grants

        var effective = await h.Service.GetEffectivePermissionsAsync(["member"]);

        Assert.Empty(effective);
    }

    // -----------------------------------------------------------------------
    // Caching / invalidation via UpsertRoleAsync
    // -----------------------------------------------------------------------

    [Fact]
    public async Task UpsertRole_InvalidatesCache_SoNextCheckReflectsNewState()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true)); // read only

        // Prime the cache with the original snapshot.
        Assert.False(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Write));

        // Grant write; UpsertRoleAsync must invalidate the cache.
        await h.Service.UpsertRoleAsync("finance", [new ModulePermissionDto
        {
            Module = Modules.Givings, CanRead = true, CanWrite = true,
        }]);

        Assert.True(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Write));
    }

    [Fact]
    public async Task UpsertRole_SuperAdmin_Throws()
    {
        var h = BuildHarness();
        await h.SeedRoleAsync("super_admin");

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => h.Service.UpsertRoleAsync("super_admin", [new ModulePermissionDto { Module = Modules.Members, CanRead = true }]));
    }

    [Fact]
    public async Task UpsertRole_UnknownRole_Throws()
    {
        var h = BuildHarness();

        await Assert.ThrowsAsync<KeyNotFoundException>(
            () => h.Service.UpsertRoleAsync("ghost", [new ModulePermissionDto { Module = Modules.Members, CanRead = true }]));
    }
}
