using System.Reflection;
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;

// [W3 — S187 2026-08-10] Khoá DRIFT menu-key/policy cho họ leaf Designer `AwV2_*`
// (dưới root "Quy trình duyệt (Mới)" = `ApprovalWorkflowsV2`).
//
// ĐÍCH của wave W3 (spec `spec-duyet-hd-10-08-2026.md:10` + `sub-plan-review-fable.md §5`):
// thêm **7 key `AwV2_Hd*`** vào `MenuKeys.All` + 7 menu-row trong `SeedMenuTreeAsync`, mirror
// TRỌN khuôn 8 key `AwV2_KhkkN{1..8}` (K3 @S164/S165), cho 7 mã quy trình duyệt HĐ
// `HD-TP · HD-GK · HD-NCC · HD-DV · HD-MB · HD-NT-NCC · HD-NT-DV`.
// Hệ quả SỐ (2 row canonical `docs/STATUS.md`): Menu keys 113 → **120** · Policies 452 → **480**
// (Policies = |All| × |Actions|, DERIVED — khoá ở `KhkkGroupMenuSeedTests` T2, xem §"chồng lấn").
//
// ── VÌ SAO FILE NÀY KHÔNG VIẾT TÊN KEY RA CHỮ ────────────────────────────────────────────
// Spec chốt "CÓ sinh 7 key `AwV2_Hd*`" nhưng KHÔNG chốt chuỗi ký tự cụ thể (`AwV2_HdTp`?
// `AwV2_HdThauPhu`? theo `ContractTypeCodes` hay theo token mã `HD-TP`?). Viết thẳng
// `MenuKeys.ApprovalWorkflowHdTp` mà lane BE đặt tên khác ⇒ LỖI BIÊN DỊCH ⇒ sập cả test-project
// ⇒ đỏ vì "test sai", đúng thứ phải tránh. Nên: **0 tham chiếu tới symbol C# chưa tồn tại** —
// họ key mới được PHÁT HIỆN bằng phép trừ trên `MenuKeys.All` (10 key cũ tham chiếu qua const
// CÓ THẬT). Tập rỗng ⇒ đỏ tại vế tiền-đề với thông điệp "chưa có key nào" (= key chưa có).
// 🔄 Cập nhật giữa lượt: lane BE ĐÃ land `MenuKeys.cs` + `ContractApprovalWorkflowFamily.cs`
// (2026-08-10) ⇒ 7 chuỗi key nay ĐỌC ĐƯỢC TỪ MÃ và được ghim VIẾT TAY ở `Expected7` (khoá drift
// tên — xem chú thích tại đó). Vẫn giữ luật "0 symbol mới": file này KHÔNG gọi
// `MenuKeys.ContractWorkflowMenuKeys` / `ContractApprovalWorkflowFamily.KeyToken` — gọi bộ sinh
// làm kỳ vọng là tautology, và tên field của lane khác đổi thì test phải ĐỎ có thông điệp, chứ
// không phải sập biên dịch cả test-project.
//
// ── CHỒNG LẤN đã tra TRƯỚC khi viết (bài S166) ───────────────────────────────────────────
//   • `ContractCatalogTests.cs:445` (T6 lane K1) — `All.Except(menuKeysInDb).BeEmpty()` phủ
//     chiều "key ∈ All ⇒ có menu-row" cho TOÀN BỘ `All` ⇒ file này KHÔNG nhân bản, chỉ thêm
//     bản gia-cố per-key (thông điệp gọi đúng tên key thiếu) + **chiều NGƯỢC chưa ai đo**.
//   • `KhkkGroupMenuSeedTests.cs:131` (T2 lane K4a) — số neo `|All|` DUY NHẤT trong `tests/`;
//     update 113 → 120 ở CHÍNH nó (không mở số neo thứ hai ở đây — 1 sự thật 1 chỗ).
//   • `grep "AwV2\|KhkkN" tests/` = **0 hit** trước lượt này ⇒ họ 10 leaf Designer đang sống
//     CHƯA có test nào. File này khoá cả họ cũ (control dương, XANH hôm nay) lẫn họ mới.
//   • Admin grant 4 cờ cho key mới: CỐ Ý KHÔNG viết — by-construction từ loop `MenuKeys.All`
//     trong `SeedAdminPermissionsAsync :2299`, và đã có chứng nhân độc lập ở ContractCatalog T6.
//   • `RevokeTemporarilyHiddenModulesAsync`: bộ lọc `:2568-2571` = `StartsWith("Hrm"|"Off") ||
//     == Personal`; site-2 `:2413` CỐ Ý loại `AwV2_*` (admin-only) ⇒ không có ca thật, không
//     viết test trang trí.
public class AwV2DesignerMenuKeyTests
{
    // Tiền tố họ leaf Designer. Root `ApprovalWorkflowsV2` = "ApprovalWorkflowsV2" (KHÔNG mang
    // tiền tố này) nên phép lọc dưới chỉ bắt LEAF.
    private const string DesignerLeafPrefix = "AwV2_";

    // Khuôn tên bắt buộc của họ mới (spec §5: "+7 key `AwV2_Hd*`").
    private const string ContractDesignerPrefix = "AwV2_Hd";

    // 10 leaf Designer ĐANG SỐNG — tham chiếu qua const CÓ THẬT (đổi tên const ⇒ lỗi biên dịch
    // ngay, đúng chỗ). Vừa là tập trừ để phát hiện họ mới, vừa là CONTROL DƯƠNG.
    private static readonly string[] LegacyDesignerLeafKeys =
    {
        MenuKeys.ApprovalWorkflowDuyetNccV2,
        MenuKeys.ApprovalWorkflowDuyetNccPhuongAnV2,
        MenuKeys.ApprovalWorkflowKhkkN1, MenuKeys.ApprovalWorkflowKhkkN2,
        MenuKeys.ApprovalWorkflowKhkkN3, MenuKeys.ApprovalWorkflowKhkkN4,
        MenuKeys.ApprovalWorkflowKhkkN5, MenuKeys.ApprovalWorkflowKhkkN6,
        MenuKeys.ApprovalWorkflowKhkkN7, MenuKeys.ApprovalWorkflowKhkkN8,
    };

    // 7 mã quy trình HĐ (lane t0 đã ghim cùng bộ ở `UpdateAwDefinitionTests.cs:1574`).
    private static readonly string[] ContractWorkflowCodes =
        { "HD-TP", "HD-GK", "HD-NCC", "HD-DV", "HD-MB", "HD-NT-NCC", "HD-NT-DV" };

    // 7 key kỳ vọng, VIẾT TAY (khuôn `KhkkGroupMenuSeedTests.Expected49`): CỐ Ý **không** gọi
    // `MenuKeys.ContractWorkflowMenuKeys` / `ContractApprovalWorkflowFamily.KeyToken` — gọi lại
    // bộ sinh là TAUTOLOGY (luật sinh sai chuỗi thì kỳ vọng sai theo, test xanh vô nghĩa).
    // Chuỗi dưới ĐỌC TỪ MÃ ĐÃ LAND @S187 W3: mã `HD-{X}` → bỏ tiền tố `HD-`, bỏ mọi `-`, GIỮ
    // hoa/thường → ghép `AwV2_Hd{token}` (`ContractApprovalWorkflowFamily.KeyToken`).
    // 🔴 Đây là chỗ khoá DRIFT TÊN: đổi luật đặt key (vd "normalize" về `AwV2_Hdtp`) mà không
    // đổi dòng này = ĐỎ — đúng ý, vì đổi key sau khi đã seed là mồ côi permission-row đã cấp
    // (bài S155) + lệch `fe-*/src/lib/menuKeys.ts` đồng bộ tay.
    private static readonly string[] Expected7 =
    {
        "AwV2_HdTP", "AwV2_HdGK", "AwV2_HdNCC", "AwV2_HdDV",
        "AwV2_HdMB", "AwV2_HdNTNCC", "AwV2_HdNTDV",
    };

    private const string ExpectedCount = "7";

    // Họ MỚI = mọi leaf `AwV2_*` trong `All` TRỪ 10 leaf cũ.
    private static string[] ContractDesignerKeysInAll() =>
        MenuKeys.All
            .Where(k => k.StartsWith(DesignerLeafPrefix, StringComparison.Ordinal))
            .Except(LegacyDesignerLeafKeys, StringComparer.Ordinal)
            .OrderBy(k => k, StringComparer.Ordinal)
            .ToArray();

    // Seeder `private static` → REFLECTION (KHÔNG sửa prod đổi visibility) — mirror
    // `KhkkGroupMenuSeedTests.InvokePrivateSeedAsync`.
    private static async Task SeedMenuAsync(TestApplicationDbContext db)
    {
        var mi = typeof(DbInitializer).GetMethod("SeedMenuTreeAsync", BindingFlags.NonPublic | BindingFlags.Static);
        mi.Should().NotBeNull("DbInitializer.SeedMenuTreeAsync phải tồn tại (private static) — đổi signature thì cập nhật test");
        await (Task)mi!.Invoke(null, new object[] { db, NullLogger.Instance })!;
    }

    private static async Task<List<MenuItem>> DesignerChildrenAsync(TestApplicationDbContext db)
    {
        var rows = await db.MenuItems.AsNoTracking()
            .Where(m => m.ParentKey == MenuKeys.ApprovalWorkflowsV2).ToListAsync();
        return rows.OrderBy(r => r.Key, StringComparer.Ordinal).ToList();
    }

    // =====================================================================================
    // T1 — `MenuKeys.All`: đúng 7 key Designer HĐ, đúng khuôn tên `AwV2_Hd*`, 10 key cũ NGUYÊN.
    //      Vế "10 key cũ nguyên" đứng TRƯỚC = CONTROL DƯƠNG cho chính phép trừ: nếu ai đổi tên
    //      họ `AwV2_KhkkN*` thì tập-trừ sẽ nhặt nhầm chúng vào "họ mới" và mọi kết luận dưới
    //      thành rác — vế này bắt đúng ca đó trước khi kịp kết luận sai.
    // =====================================================================================
    [Fact]
    public void MenuKeys_All_HasSevenContractDesignerKeys_AndKeepsTheTenLegacyOnes()
    {
        // ── CONTROL DƯƠNG: cơ sở của phép trừ còn nguyên ─────────────────────────────────
        MenuKeys.All.Should().Contain(LegacyDesignerLeafKeys,
            "10 leaf Designer đang sống (2 Duyệt NCC + 8 KHKK nhóm) phải còn trong `All` — mất/đổi tên " +
            "là (a) mồ côi permission-row đã cấp (bài S155) và (b) phép trừ tìm 'họ mới' bên dưới hỏng thước");

        LegacyDesignerLeafKeys.Should().OnlyHaveUniqueItems("10 hằng phải trỏ 10 chuỗi khác nhau");

        // ── VẾ CHÍNH: họ MỚI phải có đúng 7 thành viên ───────────────────────────────────
        var family = ContractDesignerKeysInAll();

        family.Should().HaveCount(7,
            $"W3 @S187 thêm ĐÚNG {ExpectedCount} leaf Designer cho {ContractWorkflowCodes.Length} loại HĐ " +
            $"({string.Join(" · ", ContractWorkflowCodes)}) — mỗi loại 1 quy trình V2 riêng (PA-A). " +
            "Đếm được 0 = lane BE chưa land 7 key vào `MenuKeys.All` (spec-change: test + code CÙNG wave, " +
            "rules §7). Đếm 6/8 = lệch danh sách. Số này quyết định 2 row canonical `Menu keys` (113→120) " +
            "và `Policies` (452→480 = |All|×4) ở docs/STATUS.md");

        family.Should().OnlyHaveUniqueItems(
            "key đôi trong `All` ⇒ SeedAdminPermissionsAsync chèn Permission trùng (RoleId, MenuKey) ⇒ nổ UNIQUE");

        // ── ĐỦ 7 TÊN, đúng chuỗi ký tự (khoá drift tên — xem chú thích `Expected7`) ──────
        family.Should().BeEquivalentTo(Expected7,
            $"7 key phải đúng khuôn `{ContractDesignerPrefix}{{token}}` với token = mã bỏ `HD-` và bỏ `-` " +
            $"({string.Join(" → ", ContractWorkflowCodes.Select((c, i) => c + "/" + Expected7[i]))}). " +
            "Sai 1 ký tự = menu-row lệch key với `MenuKeys.All` ⇒ Permission FK nổ lúc boot; đổi key SAU khi " +
            "đã seed = mồ côi permission-row đã cấp (bài S155) + lệch `fe-*/src/lib/menuKeys.ts` (đồng bộ tay)");
    }

    // =====================================================================================
    // T2 — MENU-ROW cho 7 key mới: có row thật, treo đúng cha `ApprovalWorkflowsV2`, và HAI
    //      CHIỀU khớp nhau với `All`:
    //        (A) key ∈ All ⇒ có menu-row  — thiếu row = Permission FK nổ NGAY lần boot đầu
    //            (`MenuKeys.cs:125-126`; chiều này `ContractCatalogTests:445` đã phủ toàn cục,
    //             ở đây gia cố per-key để thông điệp gọi đúng tên key thiếu).
    //        (B) menu-row `AwV2_*` ⇒ key ∈ All — CHƯA AI ĐO. Row có mà key ngoài `All` thì
    //            `Program.cs` KHÔNG sinh policy ⇒ mục hiện trên sidebar admin nhưng bấm vào là
    //            403/500 (cùng lớp gotcha #85: menu rộng hơn API).
    //      Thứ tự: seed → CONTROL DƯƠNG trên 10 leaf cũ → tiền-đề 7 → vế mới. Control đứng
    //      trước để lúc đỏ còn phân biệt được "seeder không chạy" với "7 key chưa có".
    // =====================================================================================
    [Fact]
    public async Task SeedMenuTree_ContractDesignerLeaves_HaveRowsUnderApprovalWorkflowsV2_AndMatchAllBothWays()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();

        await SeedMenuAsync(db);

        // ── CONTROL DƯƠNG: seeder CÓ chạy và khuôn assert dưới là thoả được ──────────────
        var root = await db.MenuItems.AsNoTracking().FirstOrDefaultAsync(m => m.Key == MenuKeys.ApprovalWorkflowsV2);
        root.Should().NotBeNull("root 'Quy trình duyệt (Mới)' phải có row — vế này đỏ = seeder không chạy, đừng đọc các vế sau");
        root!.ParentKey.Should().Be(MenuKeys.System, "root Designer treo dưới Hệ thống");

        var children = await DesignerChildrenAsync(db);
        children.Select(c => c.Key).Should().Contain(LegacyDesignerLeafKeys,
            "10 leaf Designer cũ phải có row dưới đúng root — vế này đỏ = hỏng THƯỚC (parent/seeder), " +
            "không phải chuyện của 7 key mới");

        // ── TIỀN-ĐỀ (chống vacuous): không có 7 key thì mọi vòng lặp dưới chạy 0 lần ─────
        var family = ContractDesignerKeysInAll();
        family.Should().HaveCount(7,
            $"tiền-đề: `MenuKeys.All` phải có {ExpectedCount} key `{ContractDesignerPrefix}*` (W3). " +
            "Đếm 0 = lane BE chưa land — mọi assert menu-row bên dưới sẽ đúng-vô-nghĩa nếu bỏ vế này");

        // ── (A) key ∈ All ⇒ menu-row, đúng cha, có nhãn + icon ──────────────────────────
        foreach (var key in family)
        {
            var row = children.FirstOrDefault(c => c.Key == key);
            row.Should().NotBeNull(
                $"{key} ∈ MenuKeys.All ⇒ BẮT BUỘC có menu-row trong SeedMenuTreeAsync với ParentKey=" +
                $"'{MenuKeys.ApprovalWorkflowsV2}'. Thiếu row = SeedAdminPermissionsAsync chèn Permission " +
                "→ FK MenuKey→MenuItem.Key nổ NGAY lần boot đầu (MenuKeys.cs:125-126)");
            row!.Label.Should().NotBeNullOrWhiteSpace($"{key} phải có nhãn tiếng Việt hiện trên sidebar admin");
            row.Icon.Should().NotBeNullOrWhiteSpace($"{key} phải có icon như 10 leaf anh em (sidebar lệch hình nếu trống)");
        }

        // ── (B) CHIỀU NGƯỢC: mọi menu-row họ `AwV2_*` phải nằm trong `All` ──────────────
        // Lọc TRONG BỘ NHỚ bằng Ordinal (LIKE của SQLite case-insensitive ASCII — không muốn
        // phép đo phụ thuộc collation provider; bài S166).
        var allRowKeys = await db.MenuItems.AsNoTracking().Select(m => m.Key).ToListAsync();
        var awV2RowKeys = allRowKeys.Where(k => k.StartsWith(DesignerLeafPrefix, StringComparison.Ordinal)).ToArray();

        awV2RowKeys.Should().HaveCount(17,
            $"10 leaf cũ + {ExpectedCount} leaf HĐ mới. Đếm 10 = 7 row mới chưa được seed; " +
            "đếm >17 = seed dư row (mục lạ trên sidebar admin)");

        awV2RowKeys.Except(MenuKeys.All, StringComparer.Ordinal).Should().BeEmpty(
            "menu-row họ `AwV2_*` mà key NGOÀI `MenuKeys.All` ⇒ Program.cs không sinh policy cho key đó ⇒ " +
            "mục hiện trên sidebar nhưng endpoint/gate tương ứng 403/500 (cùng lớp gotcha #85). " +
            "Vào `All` và seed row là MỘT cặp, không được lệch nửa");
    }

    // =====================================================================================
    // T3 — HÌNH SIDEBAR của cả 17 leaf: NHÃN phân biệt + ORDER phân biệt.
    //      Khối 7 dòng mới là copy-paste của khối 8 dòng `AwV2_KhkkN*` (`DbInitializer:1988-1995`)
    //      ⇒ 2 lỗi kinh điển của copy-paste khối: (i) quên sửa 1 nhãn ⇒ 2 mục cùng tên
    //      "Duyệt HĐ — …" trên sidebar admin ⇒ chọn nhầm Designer ⇒ sửa quy trình của LOẠI HĐ
    //      KHÁC; (ii) quên tăng Order ⇒ 2 mục cùng Order ⇒ thứ tự anh em bất định giữa các lần
    //      render (GetMyMenuTree sort theo Order).
    //      Chống vacuous: tiền-đề 17 leaf đứng trước (tập rỗng thì "phân biệt" đúng ở mọi thế giới).
    // =====================================================================================
    [Fact]
    public async Task SeedMenuTree_AllDesignerLeaves_HaveDistinctLabelsAndDistinctOrders()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();

        await SeedMenuAsync(db);
        var children = await DesignerChildrenAsync(db);

        children.Should().HaveCount(17,
            $"2 leaf Duyệt NCC + 8 leaf KHKK nhóm + {ExpectedCount} leaf HĐ (W3 @S187) = 17 mục Designer. " +
            "Đếm 10 = lane BE chưa land 7 menu-row `AwV2_Hd*`");

        children.Select(c => c.Label).Should().OnlyHaveUniqueItems(
            "17 mục Designer = 17 nhãn PHÂN BIỆT. Copy-paste khối 7 dòng mà quên sửa 1 nhãn ⇒ 2 mục cùng " +
            "tên trên sidebar admin ⇒ mở nhầm Designer của loại HĐ khác (sửa quy trình sai loại)");

        children.Select(c => c.Order).Should().OnlyHaveUniqueItems(
            "17 mục = 17 Order PHÂN BIỆT. Trùng Order ⇒ thứ tự anh em bất định (GetMyMenuTree sort theo " +
            "Order) ⇒ sidebar đổi chỗ giữa các lần render");
    }
}
