using System.Reflection;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Api.Authorization;
using SolutionErp.Api.Controllers;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Api;

// [K7 — S167 2026-08-01] AUTHZ 2 CHIỀU cho cầu GĐ2→GĐ3 `POST {id}/create-contract`.
//
// Khác `AuthorizePolicyRegressionTests` (chỉ đọc ATTRIBUTE bằng reflection = "cửa có khoá tên gì"),
// file này chạy **máy quyết định thật** `MenuPermissionHandler` = "khoá đó có mở/không mở cho ai".
// Hai lớp bổ nhau: pin tên khoá mà không đo quyết định thì vẫn có thể sai ở tầng dữ liệu quyền,
// và ngược lại.
//
// 🔴 ĐIỀU KIỆN LÀM PHÉP ĐO CÓ RĂNG: 2 tên policy KHÔNG hardcode — ĐỌC TỪ CHÍNH CONTROLLER
// (class-level + action-level của `CreateContract`). Ai sửa attribute thì test này đo theo cái
// MỚI, và vẫn hỏi đúng câu: "hai vế có phải HỘI không". Hardcode chuỗi ở đây là tự tay tách
// test khỏi thứ nó bảo vệ.
//
// Vì sao "hội 2 khoá" quan trọng (gotcha #85 + #82): ASP.NET Core cộng dồn `[Authorize]` class +
// action theo **AND**. Nếu ai đó gỡ 1 vế:
//   - gỡ `Contracts.Create`  ⇒ người chỉ được xem/sửa kế hoạch ký kết ĐẺ RA hợp đồng (quyền chéo);
//   - gỡ `KeHoachKyKet.Read` ⇒ người ngoài module lấy dòng của phiếu người khác đi tạo hợp đồng.
public class KhkkBridgeAuthzTests
{
    /// Nạp 2 vế khoá của endpoint cầu TỪ ĐĨA (không hardcode).
    private static (string ClassPolicy, string ActionPolicy) BridgePolicies()
    {
        var t = typeof(ContractSigningPlansController);
        var classPolicy = t.GetCustomAttributes<AuthorizeAttribute>(inherit: false)
            .FirstOrDefault()?.Policy;
        var actionPolicy = t
            .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            .First(m => m.Name == nameof(ContractSigningPlansController.CreateContract))
            .GetCustomAttributes<AuthorizeAttribute>(inherit: false).FirstOrDefault()?.Policy;

        classPolicy.Should().NotBeNullOrEmpty("vế 1 (class-level) của hội-2-khoá biến mất");
        actionPolicy.Should().NotBeNullOrEmpty("vế 2 (action-level) của hội-2-khoá biến mất");
        actionPolicy.Should().NotBe(classPolicy,
            "hai vế TRÙNG NHAU thì 'hội 2 khoá' chỉ còn là 1 khoá — cửa tự nới ra mà nhìn vẫn như cũ");
        return (classPolicy!, actionPolicy!);
    }

    /// Chạy ĐÚNG máy production: `MenuPermissionHandler` + `UserManager`/`RoleManager` thật.
    /// Trả về quyết định cho MỘT policy "{menuKey}.{action}".
    private static async Task<bool> AllowsAsync(IdentityFixture fix, User user, string policy)
    {
        var parts = policy.Split('.');
        parts.Should().HaveCount(2, "policy \"{0}\" không đúng dạng {{menuKey}}.{{action}}", policy);

        var handler = new MenuPermissionHandler(
            fix.Services.GetRequiredService<TestApplicationDbContext>(),
            fix.Services.GetRequiredService<UserManager<User>>(),
            fix.Services.GetRequiredService<RoleManager<Role>>());

        var principal = new ClaimsPrincipal(new ClaimsIdentity(
            [new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())], "test"));
        var req = new MenuPermissionRequirement(parts[0], parts[1]);
        var ctx = new AuthorizationHandlerContext([req], principal, resource: null);

        await handler.HandleAsync(ctx);
        return ctx.HasSucceeded;
    }

    /// Kết quả THẬT của endpoint = HỘI 2 vế (ASP.NET Core cộng dồn theo AND).
    private static async Task<bool> BridgeAllowedAsync(IdentityFixture fix, User user)
    {
        var (classPolicy, actionPolicy) = BridgePolicies();
        return await AllowsAsync(fix, user, classPolicy)
            && await AllowsAsync(fix, user, actionPolicy);
    }

    /// Menu row PHẢI có trước Permission: FK `Permission.MenuKey → MenuItem.Key` (Cascade).
    private static async Task SeedMenuRowsAsync(TestApplicationDbContext db, params string[] keys)
    {
        var order = 1;
        foreach (var k in keys)
        {
            if (await db.MenuItems.AnyAsync(m => m.Key == k)) continue;
            db.MenuItems.Add(new MenuItem { Key = k, Label = k, Order = order++ });
        }
        await db.SaveChangesAsync(CancellationToken.None);
    }

    /// Tạo 1 vai + user của vai đó, cấp đúng bộ cờ được liệt kê.
    private static async Task<User> UserWithAsync(
        IdentityFixture fix, TestApplicationDbContext db, string roleName,
        params (string MenuKey, string Action)[] grants)
    {
        var user = await fix.CreateUserAsync(
            $"{roleName.ToLowerInvariant()}-{Guid.NewGuid():N}@test.local", roleName,
            departmentId: null, roles: [roleName]);

        var role = await fix.Services.GetRequiredService<RoleManager<Role>>().FindByNameAsync(roleName);
        role.Should().NotBeNull();

        foreach (var g in grants.GroupBy(x => x.MenuKey))
        {
            var actions = g.Select(x => x.Action).ToHashSet();
            db.Permissions.Add(new Permission
            {
                Id = Guid.NewGuid(),
                RoleId = role!.Id,
                MenuKey = g.Key,
                CanRead = actions.Contains("Read"),
                CanCreate = actions.Contains("Create"),
                CanUpdate = actions.Contains("Update"),
                CanDelete = actions.Contains("Delete"),
            });
        }
        await db.SaveChangesAsync(CancellationToken.None);
        return user;
    }

    private static (string MenuKey, string Action) Split(string policy)
    {
        var p = policy.Split('.');
        return (p[0], p[1]);
    }

    // ===================================================================
    // (1) ĐỦ 2 VẾ → qua. Đây là ĐỐI CHỨNG DƯƠNG: thiếu nó thì 2 test 403 bên dưới
    //     vẫn xanh trong một thế giới mà KHÔNG AI vào được cửa này (403 cho tất cả).
    // ===================================================================
    [Fact]
    public async Task CauKhkkSangHd_VaiDuCaHaiVe_ThiQua()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (classPolicy, actionPolicy) = BridgePolicies();
        var (khkkKey, khkkAction) = Split(classPolicy);
        var (contractKey, contractAction) = Split(actionPolicy);

        await SeedMenuRowsAsync(db, khkkKey, contractKey);
        var user = await UserWithAsync(fix, db, "BridgeFull",
            (khkkKey, khkkAction), (contractKey, contractAction));

        (await AllowsAsync(fix, user, classPolicy)).Should().BeTrue();
        (await AllowsAsync(fix, user, actionPolicy)).Should().BeTrue();
        (await BridgeAllowedAsync(fix, user)).Should().BeTrue(
            "vai mang ĐỦ `{0}` ∧ `{1}` phải bấm được nút Đưa vào Hợp đồng", classPolicy, actionPolicy);
    }

    // ===================================================================
    // (2) THIẾU ĐÚNG 1 VẾ (không có `Contracts.Create`) → 403.
    //     Đây chính là hình dạng của LỖ mà lane BE phải vá bằng seeder: trước S167 KHÔNG vai nào
    //     mang `Contracts.Create` (`SeedAllRolesReviewReadPermissionsAsync` đặt `CanCreate = isPe` ⇒ false cho key
    //     `Contracts`) ⇒ mọi người-không-Admin đều rơi vào nhánh này.
    // ===================================================================
    [Fact]
    public async Task CauKhkkSangHd_VaiChiCoQuyenKeHoach_ThiBiChan()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (classPolicy, actionPolicy) = BridgePolicies();
        var (khkkKey, khkkAction) = Split(classPolicy);
        var (contractKey, _) = Split(actionPolicy);

        await SeedMenuRowsAsync(db, khkkKey, contractKey);
        // Cấp RỘNG TAY trên module kế hoạch (cả 4 cờ) nhưng KHÔNG có gì trên `Contracts`.
        var user = await UserWithAsync(fix, db, "KhkkOnly",
            (khkkKey, "Read"), (khkkKey, "Create"), (khkkKey, "Update"), (khkkKey, "Delete"));

        (await AllowsAsync(fix, user, classPolicy)).Should().BeTrue(
            "vế 1 PHẢI qua — nếu vế này cũng chặn thì test không đo được vế 2 nào cả");
        (await AllowsAsync(fix, user, actionPolicy)).Should().BeFalse(
            "vế 2 `{0}` là khoá của tài nguyên ĐƯỢC TẠO; quyền trên kế hoạch ký kết " +
            "KHÔNG được đẻ ra hợp đồng (gotcha #85 — không có kế-thừa key)", actionPolicy);
        (await BridgeAllowedAsync(fix, user)).Should().BeFalse("thiếu 1 vế = 403");
    }

    // ===================================================================
    // (3) THIẾU ĐÚNG 1 VẾ CÒN LẠI (không có `KeHoachKyKet.Read`) → 403.
    //     Chiều ngược: người được tạo HĐ nhưng ngoài module kế hoạch cũng KHÔNG được lấy dòng
    //     của phiếu người khác đi bắc cầu.
    // ===================================================================
    [Fact]
    public async Task CauKhkkSangHd_VaiChiCoQuyenTaoHopDong_ThiBiChan()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (classPolicy, actionPolicy) = BridgePolicies();
        var (khkkKey, _) = Split(classPolicy);
        var (contractKey, contractAction) = Split(actionPolicy);

        await SeedMenuRowsAsync(db, khkkKey, contractKey);
        var user = await UserWithAsync(fix, db, "ContractOnly", (contractKey, contractAction));

        (await AllowsAsync(fix, user, actionPolicy)).Should().BeTrue(
            "vế 2 PHẢI qua — chứng minh phép đo không xanh nhờ 'chặn tất');");
        (await AllowsAsync(fix, user, classPolicy)).Should().BeFalse();
        (await BridgeAllowedAsync(fix, user)).Should().BeFalse("thiếu vế còn lại = vẫn 403");
    }

    // ===================================================================
    // (4) Admin đi cửa bypass — KHÔNG có 1 dòng `Permissions` nào vẫn qua cả 2 vế.
    //     Khoá hành vi ĐANG CÓ (`MenuPermissionHandler.cs` nhánh `AppRoles.Admin`) để không ai
    //     "siết cho nhất quán" rồi khoá luôn Admin khỏi cầu.
    // ===================================================================
    [Fact]
    public async Task CauKhkkSangHd_Admin_KhongCanDongQuyenNao_VanQua()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var admin = await fix.CreateUserAsync(
            $"admin-{Guid.NewGuid():N}@test.local", "Admin", null, [AppRoles.Admin]);

        (await db.Permissions.CountAsync()).Should().Be(0, "cố ý 0 dòng quyền — đo đúng nhánh bypass");
        (await BridgeAllowedAsync(fix, admin)).Should().BeTrue();
    }

    // ===================================================================
    // (5) User bị vô hiệu hoá: có ĐỦ quyền nhưng `IsActive = false` → chặn cả 2 vế.
    //     Vế này bắt lỗi "nghỉ việc nhưng token còn hạn vẫn đẻ được hợp đồng".
    // ===================================================================
    [Fact]
    public async Task CauKhkkSangHd_UserBiVoHieuHoa_ThiChan_DuCoDuQuyen()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (classPolicy, actionPolicy) = BridgePolicies();
        var (khkkKey, khkkAction) = Split(classPolicy);
        var (contractKey, contractAction) = Split(actionPolicy);

        await SeedMenuRowsAsync(db, khkkKey, contractKey);
        var user = await UserWithAsync(fix, db, "BridgeDisabled",
            (khkkKey, khkkAction), (contractKey, contractAction));

        (await BridgeAllowedAsync(fix, user)).Should().BeTrue("chốt trước: lúc còn hoạt động thì QUA");

        var tracked = await db.Users.SingleAsync(u => u.Id == user.Id);
        tracked.IsActive = false;
        await db.SaveChangesAsync(CancellationToken.None);
        db.ChangeTracker.Clear();

        (await BridgeAllowedAsync(fix, user)).Should().BeFalse(
            "đổi ĐÚNG 1 biến `IsActive` ⇒ cùng bộ quyền, kết quả phải ngược lại");
    }
}
