using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Persistence;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Application;

// [S118 2026-07-14 — anh Kiệt FDC] Regression test SECURITY cho seeder MỚI
// DbInitializer.SeedProcurementMasterAccessAsync (private static). Đây là thay đổi
// security-sensitive → guard thuộc-tính-an-ninh CỐT LÕI: grant KHÔNG rò sang role khác.
//
// Bối cảnh chain (SeedAsync, order thực tế):
//   1. RevokeTemporarilyHiddenModulesAsync (S58+S92) → set 4 cờ CRUD=false cho MỌI role
//      non-Admin trên Master/Suppliers/Projects/Departments/Catalog*/Contracts/Ct_*/Hrm*/
//      Off*/Personal ("chỉ Admin thấy").
//   2. SeedProcurementMasterAccessAsync (S118) → CHẠY NGAY SAU để THẮNG revoke, mở NGOẠI-LỆ
//      CHỈ role Procurement (Phòng Cung ứng):
//        - Suppliers          = Read + Create + Update (quản lý + CÔNG BỐ NCC, S113). Delete=false.
//        - Master/Projects/Departments/Catalogs/4-Catalog-leaf = Read-only.
//      UPGRADE-ONLY: row đã có → nâng cờ desired lên true; row chưa có → insert; KHÔNG hạ cờ;
//      KHÔNG cấp Delete. (Dashboard/Reports CỐ Ý bị gỡ khỏi seeder — KHÔNG test 2 key này.)
//
// Thuộc-tính an ninh test bảo vệ:
//   T1 upgrade-path  — Procurement sau revoke→seed có ĐÚNG ma trận grant (Suppliers R+C+U D=false;
//                      8 key read-only chỉ R). Chứng seeder THẮNG revoke đúng phạm vi.
//   T2 SCOPE ISOLATION (quan trọng nhất) — role non-Admin KHÁC (DeptManager, đứng RIÊNG) VẪN bị
//                      ẩn hết sau cùng chuỗi → grant CHỈ đụng Procurement, S92 giữ nguyên role khác.
//                      Kèm CONTRAST assert Procurement THẬT được nâng (chống vacuous-pass).
//   T3 insert-path   — DB mới (chưa có row, không revoke) → seeder INSERT 9 row; Delete=false (an ninh).
//   T4 idempotent    — chạy lần 2 → end-state bất biến + KHÔNG tạo row trùng + KHÔNG chạm role khác.
//
// Kỹ thuật: 2 method là `private static` (KHÔNG sửa prod đổi visibility) → gọi qua REFLECTION
// (BindingFlags.NonPublic|Static). Signature cả 2: (ApplicationDbContext, RoleManager<Role>, ILogger).
// Test ĐÚNG behavior code thật, KHÔNG re-implement predicate/grant-set.
//
// FK (PermissionConfiguration): Permission.MenuKey → MenuItem.Key (Cascade) + RoleId → Role
// (Cascade) → PHẢI seed MenuItem rows + Role TRƯỚC khi seed Permission (nếu không SQLite FK Error 19).
// Revoke skip nếu Admin role chưa seed → luôn seed Admin dù test không assert Admin.
public class ProcurementMasterAccessSeedTests
{
    // Suppliers = key DUY NHẤT được Read+Create+Update (quản lý + công bố NCC — S113). Delete=false.
    private const string SuppliersKey = MenuKeys.Suppliers;

    // 8 key CHỈ đọc (Read-only) — seeder chỉ nâng CanRead.
    private static readonly string[] ReadOnlyKeys =
    {
        MenuKeys.Master,
        MenuKeys.Projects,
        MenuKeys.Departments,
        MenuKeys.Catalogs,
        MenuKeys.CatalogUnits,
        MenuKeys.CatalogMaterials,
        MenuKeys.CatalogServices,
        MenuKeys.CatalogWorkItems,
    };

    // Toàn bộ 9 key seeder đụng (Suppliers + 8 read-only). Dùng const/factory MenuKeys
    // (KHÔNG hardcode string) để bắt đồng-bộ nếu key đổi.
    private static readonly string[] AllGrantKeys =
        ReadOnlyKeys.Append(SuppliersKey).ToArray();

    // ── Reflection invoke private static (ApplicationDbContext, RoleManager<Role>, ILogger) ──
    private static async Task InvokePrivateStaticAsync(IdentityFixture fix, string methodName)
    {
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
        var mi = typeof(DbInitializer).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
        mi.Should().NotBeNull(
            $"DbInitializer.{methodName} phải tồn tại (private static) — đổi signature thì cập nhật test");
        var task = (Task)mi!.Invoke(null, new object[] { db, rm, NullLogger.Instance })!;
        await task;
    }

    private static Task InvokeRevokeAsync(IdentityFixture fix) =>
        InvokePrivateStaticAsync(fix, "RevokeTemporarilyHiddenModulesAsync");

    private static Task InvokeSeedProcurementAsync(IdentityFixture fix) =>
        InvokePrivateStaticAsync(fix, "SeedProcurementMasterAccessAsync");

    // Seed roles: Admin (để Revoke KHÔNG skip) + Procurement (target grant) + DeptManager
    // (control isolation — đứng RIÊNG, KHÔNG kèm Procurement). Trả (adminId, procurementId, otherId).
    private static async Task<(Guid adminId, Guid procurementId, Guid otherId)> SeedRolesAsync(IdentityFixture fix)
    {
        var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
        var admin = new Role { Id = Guid.NewGuid(), Name = AppRoles.Admin };
        var procurement = new Role { Id = Guid.NewGuid(), Name = AppRoles.Procurement };
        var other = new Role { Id = Guid.NewGuid(), Name = AppRoles.DeptManager };
        await rm.CreateAsync(admin);
        await rm.CreateAsync(procurement);
        await rm.CreateAsync(other);
        return (admin.Id, procurement.Id, other.Id);
    }

    // Seed MenuItem rows (FK target cho Permission.MenuKey — cả pre-grant lẫn seeder-insert cần).
    private static async Task SeedMenuItemsAsync(TestApplicationDbContext db)
    {
        foreach (var key in AllGrantKeys)
            db.MenuItems.Add(new MenuItem { Key = key, Label = key });
        await db.SaveChangesAsync(CancellationToken.None);
    }

    private static void AddPerm(
        TestApplicationDbContext db, Guid roleId, string menuKey,
        bool canRead = false, bool canCreate = false, bool canUpdate = false, bool canDelete = false)
        => db.Permissions.Add(new Permission
        {
            RoleId = roleId,
            MenuKey = menuKey,
            CanRead = canRead,
            CanCreate = canCreate,
            CanUpdate = canUpdate,
            CanDelete = canDelete,
        });

    private static async Task<Permission?> GetPermAsync(TestApplicationDbContext db, Guid roleId, string menuKey)
        => await db.Permissions.AsNoTracking()
            .FirstOrDefaultAsync(p => p.RoleId == roleId && p.MenuKey == menuKey);

    private static async Task AssertFlagsAsync(
        TestApplicationDbContext db, Guid roleId, string key,
        bool r, bool c, bool u, bool d, string because)
    {
        var row = await GetPermAsync(db, roleId, key);
        row.Should().NotBeNull($"{key}: {because}");
        row!.CanRead.Should().Be(r, $"{key}.CanRead — {because}");
        row.CanCreate.Should().Be(c, $"{key}.CanCreate — {because}");
        row.CanUpdate.Should().Be(u, $"{key}.CanUpdate — {because}");
        row.CanDelete.Should().Be(d, $"{key}.CanDelete — {because}");
    }

    // Ma trận grant kỳ vọng cho Procurement sau khi seeder chạy: Suppliers R+C+U (D=false),
    // 8 key còn lại chỉ Read. Dùng chung cho T1/T2-contrast/T3/T4.
    private static async Task AssertProcurementGrantMatrixAsync(TestApplicationDbContext db, Guid procurementId)
    {
        await AssertFlagsAsync(db, procurementId, SuppliersKey,
            r: true, c: true, u: true, d: false,
            "Suppliers phải R+C+U cho Procurement (quản lý + CÔNG BỐ NCC — S113), Delete=false");

        foreach (var key in ReadOnlyKeys)
            await AssertFlagsAsync(db, procurementId, key,
                r: true, c: false, u: false, d: false,
                "read-only grant — CHỈ CanRead, KHÔNG Create/Update/Delete");
    }

    // T1 — UPGRADE PATH (chain prod-realistic): Procurement từng có CanRead → revoke S92 hạ false
    // → seeder S118 nâng lại đúng ma trận. Chứng seeder THẮNG revoke, đúng phạm vi cờ.
    [Fact]
    public async Task Procurement_AfterRevokeThenSeed_HasExactGrantMatrix()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (_, procurementId, _) = await SeedRolesAsync(fix);
        await SeedMenuItemsAsync(db);

        // Procurement từng có CanRead trên cả 9 key (grant cũ trước S92) → revoke có gì để hạ.
        foreach (var key in AllGrantKeys)
            AddPerm(db, procurementId, key, canRead: true);
        await db.SaveChangesAsync(CancellationToken.None);

        await InvokeRevokeAsync(fix);          // S92: 9 key CanRead=false cho non-Admin
        await InvokeSeedProcurementAsync(fix); // S118: NGOẠI-LỆ Procurement — nâng lại

        await AssertProcurementGrantMatrixAsync(db, procurementId);
    }

    // T2 — ⭐ SCOPE ISOLATION (thuộc-tính an ninh QUAN TRỌNG NHẤT): role non-Admin KHÁC
    // (DeptManager, đứng RIÊNG — KHÔNG kèm Procurement) VẪN có Master/Suppliers CanRead=false
    // sau cùng chuỗi seed → grant S118 CHỈ đụng Procurement, S92 "chỉ Admin thấy" giữ nguyên.
    [Fact]
    public async Task SeedProcurement_DoesNotLeakToOtherNonAdminRole()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (_, procurementId, otherId) = await SeedRolesAsync(fix);
        await SeedMenuItemsAsync(db);

        // CẢ Procurement + DeptManager từng có CanRead trên 9 key (grant cũ) → revoke hạ CẢ HAI.
        foreach (var key in AllGrantKeys)
        {
            AddPerm(db, procurementId, key, canRead: true);
            AddPerm(db, otherId, key, canRead: true);
        }
        await db.SaveChangesAsync(CancellationToken.None);

        await InvokeRevokeAsync(fix);          // revoke CẢ HAI role về false
        await InvokeSeedProcurementAsync(fix); // CHỈ nâng lại Procurement

        // ⭐ ISOLATION (đọc lại theo S159): revoker KHÔNG còn chạm Master-set ⇒ CanRead=true
        // pre-grant của DeptManager GIỮ NGUYÊN (chính sách all-role read — owner 2026-07-29
        // supersede S92). Isolation của Procurement-seed nay đo bằng CỜ CAO: seeder chỉ nâng
        // CanCreate/Update cho Procurement, KHÔNG rò cờ cao sang role khác.
        foreach (var key in AllGrantKeys)
            await AssertFlagsAsync(db, otherId, key,
                r: true, c: false, u: false, d: false,
                "S159 all-role read GIỮ; cờ cao KHÔNG rò sang DeptManager từ Procurement-grant");

        // CONTRAST (chống vacuous-pass): Procurement THẬT SỰ được nâng → chứng seeder CÓ chạy,
        // nên isolation ở trên KHÔNG phải "false vì seeder no-op".
        await AssertProcurementGrantMatrixAsync(db, procurementId);
    }

    // T3 — INSERT PATH (DB mới): chưa có Permission row + KHÔNG revoke → seeder phải INSERT 9 row.
    // Chốt security: hàng insert KHÔNG cấp Delete (CanDelete=false trên cả 9).
    [Fact]
    public async Task SeedProcurement_InsertPath_FreshDb_CreatesRowsWithoutDelete()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (_, procurementId, _) = await SeedRolesAsync(fix);
        await SeedMenuItemsAsync(db);
        // KHÔNG pre-grant + KHÔNG revoke → nhánh insert (db.Permissions.Add) của seeder.

        await InvokeSeedProcurementAsync(fix);

        await AssertProcurementGrantMatrixAsync(db, procurementId);

        var count = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId);
        count.Should().Be(AllGrantKeys.Length, "seeder insert đúng 9 row (Suppliers + 8 read-only), không dư");
    }

    // T4 — IDEMPOTENT: chạy seeder lần 2 → end-state bất biến + KHÔNG tạo row trùng + KHÔNG chạm role khác.
    [Fact]
    public async Task SeedProcurement_Idempotent_SecondRunNoChange()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (_, procurementId, otherId) = await SeedRolesAsync(fix);
        await SeedMenuItemsAsync(db);

        foreach (var key in AllGrantKeys)
        {
            AddPerm(db, procurementId, key, canRead: true);
            AddPerm(db, otherId, key, canRead: true);
        }
        await db.SaveChangesAsync(CancellationToken.None);

        await InvokeRevokeAsync(fix);
        await InvokeSeedProcurementAsync(fix);
        await InvokeSeedProcurementAsync(fix);  // ⭐ chạy lần 2 — end-state phải bất biến

        await AssertProcurementGrantMatrixAsync(db, procurementId);

        // Không nhân đôi row Procurement.
        var procCount = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId);
        procCount.Should().Be(AllGrantKeys.Length, "idempotent — chạy lại KHÔNG tạo row trùng");

        // Isolation bền vững sau lần 2 (S159: CanRead all-role GIỮ true; cờ cao vẫn false).
        foreach (var key in AllGrantKeys)
            await AssertFlagsAsync(db, otherId, key,
                r: true, c: false, u: false, d: false,
                "idempotent — lần seed thứ 2 KHÔNG rò CỜ CAO sang DeptManager (CanRead all-role S159 giữ)");
    }
}
