using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Contracts;
using SolutionErp.Application.Contracts.Dtos;
using SolutionErp.Domain.Contracts;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Application;

// [W7 ĐỢT-2 — S161 2026-07-29] Bản cứng `Hdc_*`: lọc theo LOẠI + PHASE, và cờ badge
// "Đã lưu bản cứng".
// Spec: `runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w7-ban-cung-hdc-29-07-2026.md` §③-B.
// Timing = **test-after** (đợt-1 BE+FE đã land, suite 587 xanh) ⇒ 3 test này phải XANH ngay.
// Đỏ ở đây = hồi quy thật, KHÔNG được sửa test cho vừa.
//
// 3 tên PIN theo spec (`:27`) — thiếu/đổi 1 chữ là TRƯỢT acceptance:
//   T1 `ListHardCopies_ReturnsOnlyContracts_InDaPhatHanhPhase`
//   T2 `ListHardCopies_FiltersByContractType`   (review F-C1 — chứng `type` LỌC THẬT)
//   T3 `HardCopyBadge_DerivesFromSealedCopyAttachment`
//
// ── 4 quyết định hạ tầng (điều kiện để phép đo CÓ NGHĨA) ──────────────────────────
//
// (1) 🔴 Đo bằng CẢ HAI loại actor. Filter `type` nằm NGOÀI khối IDOR
//     (`ContractFeatures.cs:327` vs khối `:302-319`). Ai đó dời nó VÀO TRONG nhánh
//     `if (!admin)` thì bản chỉ-đo-Director vẫn xanh, bản chỉ-đo-admin vẫn xanh ở chiều
//     ngược lại. Nên T1/T2 chạy phép đo trên admin (đo THUẦN cái filter, không bị IDOR
//     che) rồi lặp lại trên Director non-admin (vai THẬT của trang bản cứng).
//
// (2) 🔴 Assert `Total` chứ không chỉ `Items`. `CountAsync` đứng SAU mọi `Where`
//     (`ContractFeatures.cs:340`) ⇒ `Total` là dấu vân tay của lọc SERVER-SIDE. Một bản
//     "lấy pageSize:100 rồi lọc phía client" (đúng thứ spec §②-2b cấm) cho `Items` y hệt
//     nhưng `Total` = cả thế giới. Đây là assert phân biệt được hai cài đặt đó.
//
// (3) `TestApplicationDbContext` KHÔNG wire `AuditingInterceptor` ⇒ `Remove()` = xoá CỨNG.
//     Khối xoá-mềm ở T3 set thẳng `IsDeleted = true` (bài S155: đo "xoá mềm" trên xoá cứng
//     = đo nhầm cơ chế — rào thật là global filter che dòng, không phải dòng biến mất).
//
// (4) Mỗi test dựng `IdentityFixture` RIÊNG (SQLite :memory: riêng) ⇒ thế giới ĐÓNG, nên
//     `BeEquivalentTo(<tập đủ>)` mới là phép đo hai chiều (thừa cũng đỏ, thiếu cũng đỏ).
//
// ⚠️ `ContractAttachment : BaseEntity` (`ContractAttachment.cs:13`) — **KHÔNG có `IsDeleted`**,
//     config không `HasQueryFilter` (`ContractConfiguration.cs:78-93`), xoá đính kèm là HARD
//     delete (`ContractAttachmentFeatures.cs:143`). Nên không có (và không thể có) vế
//     `!a.IsDeleted` ở đâu trong file này.
public class ContractHardCopyTests
{
    private static readonly DateTime Clock2026 = new(2026, 7, 29, 3, 0, 0, DateTimeKind.Utc);

    // ===================================================================
    // Seed helpers
    // ===================================================================

    private static TestCurrentUser As(User u, params string[] roles)
        => new(u.Id, u.FullName, u.Email, roles);

    private static async Task<(Supplier sup, Project proj)> SeedMasterAsync(TestApplicationDbContext db)
    {
        var sup = new Supplier
        {
            Id = Guid.NewGuid(),
            Code = "BTBM",
            Name = "NCC Bê tông Bình Minh",
            Type = SupplierType.NhaThauPhu,
        };
        var proj = new Project { Id = Guid.NewGuid(), Code = "FLOCK01", Name = "Dự án FLOCK 01" };
        db.Suppliers.Add(sup);
        db.Projects.Add(proj);
        await db.SaveChangesAsync(CancellationToken.None);
        return (sup, proj);
    }

    /// Khuôn `ContractV2PipelineTests.AddContractAsync` nhưng **nhận `ContractType`** — W7 đo
    /// chính trục LOẠI HĐ nên loại không thể hardcode như W6.
    private static async Task<Contract> AddContractAsync(
        TestApplicationDbContext db,
        Supplier sup,
        Project proj,
        Guid drafterId,
        ContractType type,
        ContractPhase phase,
        string tenHopDong)
    {
        var c = new Contract
        {
            Id = Guid.NewGuid(),
            Type = type,
            Phase = phase,
            SupplierId = sup.Id,
            ProjectId = proj.Id,
            DrafterUserId = drafterId,
            TenHopDong = tenHopDong,
            GiaTri = 100_000_000m,
        };
        db.Contracts.Add(c);
        await db.SaveChangesAsync(CancellationToken.None);
        return c;
    }

    /// Seed thẳng `ContractAttachments` — entity là `BaseEntity`, không interceptor, không
    /// soft-delete. 3 cột `FileName/StoragePath/ContentType` là `IsRequired()`
    /// (`ContractConfiguration.cs:84-86`) nên phải có giá trị thật.
    private static async Task<ContractAttachment> AddAttachmentAsync(
        TestApplicationDbContext db, Contract c, AttachmentPurpose purpose, string? note = null)
    {
        var att = new ContractAttachment
        {
            Id = Guid.NewGuid(),
            ContractId = c.Id,
            FileName = $"{purpose}-{Guid.NewGuid():N}.pdf",
            StoragePath = $"uploads/contracts/{c.Id}/{purpose}.pdf",
            ContentType = "application/pdf",
            FileSize = 1024,
            Purpose = purpose,
            Note = note,
        };
        db.ContractAttachments.Add(att);
        await db.SaveChangesAsync(CancellationToken.None);
        return att;
    }

    // ===================================================================
    // Act helpers — gọi HANDLER THẬT, không mock
    // ===================================================================

    /// Đúng đường FE 7 leaf `Hdc_*` gọi: `GET /api/contracts?type={1..7}&phase=9`
    /// (`ContractsController.cs:25,27` dựng query bằng VỊ TRÍ `(phase, supplierId, projectId, type)`).
    private static Task<PagedResult<ContractListItemDto>> ListAsync(
        TestApplicationDbContext db, TestCurrentUser actor,
        ContractPhase? phase = null, ContractType? type = null)
        => new ListContractsQueryHandler(db, actor)
            .Handle(new ListContractsQuery(Phase: phase, Type: type) { PageSize = 200 }, CancellationToken.None);

    private static Task<PagedResult<ContractListItemDto>> ListDeletedAsync(
        TestApplicationDbContext db, TestCurrentUser actor)
        => new ListDeletedContractsQueryHandler(db, actor)
            .Handle(new ListDeletedContractsQuery() { PageSize = 200 }, CancellationToken.None);

    private static Task<List<ContractListItemDto>> InboxAsync(
        TestApplicationDbContext db, TestCurrentUser actor)
        => new GetMyInboxQueryHandler(db, actor).Handle(new GetMyInboxQuery(), CancellationToken.None);

    // ===================================================================
    // (T1) Trang bản cứng chỉ liệt HĐ **đã phát hành**. Spec §③-B `:27` tên #1.
    //
    // Thế giới cố ý rải ĐỦ 8 phase trên CÙNG MỘT LOẠI HĐ, trong đó 3 hàng xóm nguy hiểm:
    //   • `DangDongDau = 8` — tên của nó CHÍNH LÀ khâu bản cứng b.20 ("đóng dấu"). Một cài
    //     đặt kiểu "phase bản-cứng" / `Phase >= 8` nuốt gọn nó. Không seed = lỗi vô hình.
    //   • `TraLai = 98` + `TuChoi = 99` — số LỚN HƠN 9, nên mọi so sánh `>=` sai đều lộ.
    //   • `ChoDuyet = 10` — cũng lớn hơn 9, và là phase đông dòng nhất trong prod.
    //
    // 2 HĐ `DaPhatHanh` (không phải 1) ⇒ loại ca "trả đúng một dòng do may".
    //
    // Actor chính là ADMIN có chủ đích: admin bỏ qua khối IDOR (`ContractFeatures.cs:302`)
    // nên thứ DUY NHẤT thu hẹp tập là param `phase` — đo đúng cái filter, không phải đo
    // gián tiếp qua quyền. Vế vai THẬT (Director) đo riêng ở khối 3.
    // ===================================================================
    [Fact]
    public async Task ListHardCopies_ReturnsOnlyContracts_InDaPhatHanhPhase()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (sup, proj) = await SeedMasterAsync(db);

        var drafter = await fix.CreateUserAsync("w7t1-drafter@test.local", "BCH soạn HĐ", null, []);
        var adminUser = await fix.CreateUserAsync("w7t1-admin@test.local", "Quản trị hệ thống", null, [AppRoles.Admin]);
        var directorUser = await fix.CreateUserAsync("w7t1-dir@test.local", "Giám đốc ký", null, [AppRoles.Director]);

        const ContractType type = ContractType.HopDongThauPhu;
        var published1 = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DaPhatHanh, "HĐ đã phát hành 1");
        var published2 = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DaPhatHanh, "HĐ đã phát hành 2");
        var dongDau = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DangDongDau, "HĐ đang đóng dấu (legacy 8)");
        var trinhKy = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DangTrinhKy, "HĐ đang trình ký (legacy 7)");
        var choDuyet = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.ChoDuyet, "HĐ chờ duyệt");
        var draft = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DangSoanThao, "HĐ còn Nháp");
        var traLai = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.TraLai, "HĐ bị trả lại");
        var tuChoi = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.TuChoi, "HĐ bị từ chối");

        var admin = As(adminUser, AppRoles.Admin);

        // ---- CHỨNG NHÂN DƯƠNG trước: không lọc ⇒ thấy ĐỦ 8. Thiếu vế này thì mọi assert
        //      "chỉ có phase 9" ở dưới vẫn xanh khi handler hỏng-luôn và trả rỗng/thiếu.
        var all = await ListAsync(db, admin);
        all.Items.Select(i => i.Id).Should().BeEquivalentTo(
            new[] { published1.Id, published2.Id, dongDau.Id, trinhKy.Id, choDuyet.Id, draft.Id, traLai.Id, tuChoi.Id },
            "admin không lọc phải thấy toàn bộ thế giới 8 HĐ — đây là mốc để biết việc thu hẹp "
            + "ở dưới đến TỪ PARAM chứ không phải từ một handler đang hỏng");
        all.Total.Should().Be(8);

        // ---- Vế chính: `?phase=9` ⇒ ĐÚNG tập 2 HĐ đã phát hành, không hơn không kém.
        var hardCopies = await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh);
        hardCopies.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { published1.Id, published2.Id },
            "trang bản cứng (GĐ4) chỉ theo dõi HĐ ĐÃ PHÁT HÀNH; lọt `DangDongDau` (phase 8 — "
            + "tên đúng là 'đóng dấu', hàng xóm dễ nhầm nhất) hoặc `ChoDuyet/TraLai/TuChoi` "
            + "(đều là số LỚN HƠN 9) nghĩa là điều kiện phase đang so sánh kiểu `>=` chứ không phải `==`");
        hardCopies.Total.Should().Be(2, "Total tính SAU filter — bằng 8 nghĩa là đang lọc phía client sau khi lấy cả trang");

        // Bất biến ở mức TỪNG DÒNG (acceptance §③-B `:30` 'mọi item phase==9').
        hardCopies.Items.Should().OnlyContain(i => i.Phase == ContractPhase.DaPhatHanh);
        hardCopies.Items.Should().HaveCount(2);

        // ---- Khối 3: VAI THẬT (acceptance §③-B `:31`) — Director non-admin, KHÔNG rỗng.
        var director = As(directorUser, AppRoles.Director);
        director.Roles.Should().NotContain(AppRoles.Admin, "vai thật của trang bản cứng KHÔNG phải admin");

        // Cặp 1-biến trên CHÍNH actor đó: bỏ `phase` ⇒ thấy thêm HĐ `DangTrinhKy` (vế role
        // legacy `GetEligiblePhases(Director)`); thêm `phase=9` ⇒ đúng HĐ đó rụng đi. Nếu
        // hai lần gọi ra cùng một tập thì param `phase` chẳng làm gì cả với vai này.
        var dirNoFilter = await ListAsync(db, director);
        dirNoFilter.Items.Select(i => i.Id).Should().Contain(trinhKy.Id,
            "Director thấy HĐ `DangTrinhKy` theo vế role legacy — mốc đối chứng cho lần lọc dưới");

        var dirHardCopies = await ListAsync(db, director, phase: ContractPhase.DaPhatHanh, type: type);
        dirHardCopies.Items.Should().NotBeEmpty(
            "vai bản-cứng THẬT phải thấy được danh sách (acceptance đòi 'không rỗng/không 403')");
        dirHardCopies.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { published1.Id, published2.Id });
        dirHardCopies.Items.Select(i => i.Id).Should().NotContain(trinhKy.Id,
            "cùng actor, chỉ thêm ĐÚNG MỘT biến `phase=9`, HĐ đang trình ký phải rụng — chứng "
            + "việc thu hẹp là của param, không phải của quyền");
        dirHardCopies.Total.Should().Be(2);
    }

    // ===================================================================
    // (T2) Param `type` LỌC THẬT — 2 loại ra 2 danh sách KHÁC NHAU. Spec §③-B `:27` tên #2,
    //      acceptance `:30` (review F-C1).
    //
    // Lỗi mà test này tồn tại để bắt: 7 leaf `Hdc_*` hiện y hệt nhau vì BE bỏ qua `type`
    // (trước W7 `GET /api/contracts` KHÔNG CÓ param này) — nhìn trên UI thì mỗi trang vẫn
    // "có dữ liệu", trông như chạy đúng.
    //
    // Thế giới trộn 3 loại × 2 phase để 2 filter phải AND với nhau chứ không đè nhau.
    // ===================================================================
    [Fact]
    public async Task ListHardCopies_FiltersByContractType()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (sup, proj) = await SeedMasterAsync(db);

        var drafter = await fix.CreateUserAsync("w7t2-drafter@test.local", "BCH soạn HĐ", null, []);
        var adminUser = await fix.CreateUserAsync("w7t2-admin@test.local", "Quản trị hệ thống", null, [AppRoles.Admin]);
        var hrUser = await fix.CreateUserAsync("w7t2-hra@test.local", "HC-NS đóng dấu", null, [AppRoles.HrAdmin]);

        var t1a = await AddContractAsync(db, sup, proj, drafter.Id, ContractType.HopDongThauPhu, ContractPhase.DaPhatHanh, "Thầu phụ A");
        var t1b = await AddContractAsync(db, sup, proj, drafter.Id, ContractType.HopDongThauPhu, ContractPhase.DaPhatHanh, "Thầu phụ B");
        var t2 = await AddContractAsync(db, sup, proj, drafter.Id, ContractType.HopDongGiaoKhoan, ContractPhase.DaPhatHanh, "Giao khoán A");
        var t5 = await AddContractAsync(db, sup, proj, drafter.Id, ContractType.HopDongMuaBan, ContractPhase.DaPhatHanh, "Mua bán A");
        var t1Pending = await AddContractAsync(db, sup, proj, drafter.Id, ContractType.HopDongThauPhu, ContractPhase.ChoDuyet, "Thầu phụ chờ duyệt");

        var admin = As(adminUser, AppRoles.Admin);

        // ---- CHỨNG NHÂN DƯƠNG: `?phase=9` KHÔNG kèm `type` ⇒ đủ 4 HĐ của 3 loại.
        var allPublished = await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh);
        allPublished.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { t1a.Id, t1b.Id, t2.Id, t5.Id },
            "bỏ `type` thì trang bản cứng thấy cả 3 loại — mốc để chứng việc tách nhỏ ở dưới là do PARAM");
        allPublished.Total.Should().Be(4);

        // ---- Vế chính: 2 loại ⇒ 2 danh sách KHÁC NHAU (literal acceptance `:30`).
        var listType1 = await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh, type: ContractType.HopDongThauPhu);
        var listType2 = await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh, type: ContractType.HopDongGiaoKhoan);

        listType1.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { t1a.Id, t1b.Id },
            "leaf 'HĐ Thầu phụ - Bảng cứng' chỉ được thấy HĐ Thầu phụ; thấy đủ 4 nghĩa là "
            + "`type` bị bỏ qua và 7 leaf `Hdc_*` sẽ hiện TRÙNG NHAU (đúng lỗi F-C1)");
        listType2.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { t2.Id });
        listType1.Items.Select(i => i.Id).Should().NotIntersectWith(listType2.Items.Select(i => i.Id),
            "acceptance đòi ĐÚNG chữ '2 loại KHÁC NHAU ra 2 danh sách KHÁC NHAU'");

        // Bất biến từng dòng — chặn ca "lọt 1 dòng loại khác ở trang sau".
        listType1.Items.Should().OnlyContain(i => i.Type == ContractType.HopDongThauPhu);
        listType2.Items.Should().OnlyContain(i => i.Type == ContractType.HopDongGiaoKhoan);

        // ---- 🔴 `Total` = dấu vân tay của lọc SERVER-SIDE (xem ghi chú hạ tầng (2)).
        listType1.Total.Should().Be(2,
            "`Total` đếm SAU `Where` (`ContractFeatures.cs:340`) ⇒ phải là 2 chứ không phải 4/5. "
            + "Bằng 4 nghĩa là dòng đã lấy về rồi mới lọc — đúng cách làm mà spec §②-2b cấm "
            + "('KHÔNG lọc client-side pageSize:100', trần 100 dòng/loại)");
        listType2.Total.Should().Be(1);

        // ---- `type` và `phase` AND độc lập: `type=1` không kèm phase ⇒ có cả HĐ ChoDuyet.
        var type1AnyPhase = await ListAsync(db, admin, type: ContractType.HopDongThauPhu);
        type1AnyPhase.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { t1a.Id, t1b.Id, t1Pending.Id },
            "`type` chỉ được lọc LOẠI; nó mà tự kéo theo phase thì hai filter đang dính nhau");

        // ---- Lặp phép đo với VAI THẬT non-admin (xem ghi chú hạ tầng (1)): nếu ai đó dời
        //      dòng filter `type` vào trong khối `if (!admin)` thì khối admin ở trên đỏ; dời
        //      ra chỗ chỉ chạy cho admin thì khối này đỏ. Hai khối khoá cả hai chiều.
        var hra = As(hrUser, AppRoles.HrAdmin);
        hra.Roles.Should().NotContain(AppRoles.Admin);
        var hraType2 = await ListAsync(db, hra, phase: ContractPhase.DaPhatHanh, type: ContractType.HopDongGiaoKhoan);
        hraType2.Items.Select(i => i.Id).Should().BeEquivalentTo(new[] { t2.Id },
            "vai bản-cứng HrAdmin (b.20 đóng dấu) cũng phải nhận đúng 1 loại — filter `type` "
            + "sống ở CẢ nhánh non-admin");
        hraType2.Total.Should().Be(1);

        // ---- Ca ÂM acceptance `:32`: loại chưa có HĐ phát hành ⇒ rỗng, KHÔNG văng.
        var emptyLeaf = await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh,
            type: ContractType.HopDongNguyenTacDichVu);
        emptyLeaf.Items.Should().BeEmpty("leaf loại chưa có HĐ nào phát hành ⇒ empty-state, không phải lỗi");
        emptyLeaf.Total.Should().Be(0, "và Total phải là 0 để FE phân trang không vẽ trang ma");
    }

    // ===================================================================
    // (T3) Badge "Đã lưu bản cứng" derive từ attachment `SealedCopy`. Spec §③-B `:27` tên #3.
    //
    // 🔴 Ca âm đắt nhất là `ScannedSigned = 2`: spec `:18` + gotcha #71 CẤM derive từ nó
    // (=2 nghĩa "scan HĐ có chữ ký NCC ở phase `DangInKy`" — chữ ký NCC, chưa đóng dấu,
    // chưa phải bộ cứng đã lưu). Cài `Purpose >= ScannedSigned` hay `Attachments.Any()`
    // (hai cách viết tự nhiên nhất) cho T3 xanh ở mọi ca KHÁC và chỉ vỡ ở đây.
    //
    // Cả 3 khối đều xoay quanh nguyên tắc: mọi `false` phải có chứng nhân là seed CÓ THẬT.
    // ===================================================================
    [Fact]
    public async Task HardCopyBadge_DerivesFromSealedCopyAttachment()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var (sup, proj) = await SeedMasterAsync(db);

        var drafter = await fix.CreateUserAsync("w7t3-drafter@test.local", "BCH soạn HĐ", null, []);
        var adminUser = await fix.CreateUserAsync("w7t3-admin@test.local", "Quản trị hệ thống", null, [AppRoles.Admin]);
        var admin = As(adminUser, AppRoles.Admin);

        const ContractType type = ContractType.HopDongThauPhu;
        var cSealed = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DaPhatHanh, "HĐ đã lưu bản cứng");
        var cOther = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DaPhatHanh, "HĐ chỉ có file khác");
        var cNone = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.DaPhatHanh, "HĐ chưa có file nào");

        await AddAttachmentAsync(db, cSealed, AttachmentPurpose.SealedCopy, note: "2 bộ gốc, lưu tủ P.HC");
        await AddAttachmentAsync(db, cOther, AttachmentPurpose.DraftExport);
        await AddAttachmentAsync(db, cOther, AttachmentPurpose.ScannedSigned);   // 🔴 gotcha #71
        await AddAttachmentAsync(db, cOther, AttachmentPurpose.Other);

        // ---- Sanity chống vacuous: `false` của `cOther` phải là "lọc đúng", KHÔNG phải
        //      "seed hỏng nên chẳng có gì". Đếm từ DB, không tin lời gọi seed.
        (await db.ContractAttachments.CountAsync(a => a.ContractId == cOther.Id))
            .Should().Be(3, "ca âm chỉ có nghĩa khi HĐ đó THỰC SỰ có 3 đính kèm (chỉ thiếu đúng purpose=3)");
        (await db.ContractAttachments.CountAsync(a => a.ContractId == cNone.Id)).Should().Be(0);

        // ---- Vế chính.
        var badges = (await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh))
            .Items.ToDictionary(i => i.Id, i => i.HasSealedCopy);

        badges.Should().HaveCount(3, "cả 3 HĐ phải nằm trong danh sách thì mới đọc được cờ của chúng");
        badges[cSealed.Id].Should().BeTrue(
            "có đính kèm `SealedCopy` (=3) ⇒ badge 'Đã lưu bản cứng' phải bật — đây là mốc DUY NHẤT "
            + "của mô hình 1-mốc (@S161): upload scan bộ cứng xong là xem như xong");
        badges[cOther.Id].Should().BeFalse(
            "HĐ này có `DraftExport(1)` + `ScannedSigned(2)` + `Other(99)` nhưng KHÔNG có `SealedCopy(3)`. "
            + "Bật cờ ở đây = đang derive từ `ScannedSigned` (scan chữ ký NCC ở `DangInKy` — gotcha #71, "
            + "spec `:18` cấm) hoặc từ 'có đính kèm bất kỳ'; cả hai đều báo 'đã lưu bản cứng' cho HĐ "
            + "chưa hề có bộ cứng nào");
        badges[cNone.Id].Should().BeFalse("không có đính kèm nào thì không thể có bản cứng");

        // ---- LẬT 1 BIẾN: thêm đúng 1 đính kèm purpose=3 cho `cOther` ⇒ cờ phải bật.
        //      Đây là chứng nhân dương cho toàn bộ các `BeFalse` ở trên: nếu cờ hỏng-luôn
        //      (luôn false) thì mọi assert âm vẫn xanh, và chỉ ca này bắt được.
        await AddAttachmentAsync(db, cOther, AttachmentPurpose.SealedCopy);
        var afterUpload = (await ListAsync(db, admin, phase: ContractPhase.DaPhatHanh))
            .Items.Single(i => i.Id == cOther.Id);
        afterUpload.HasSealedCopy.Should().BeTrue(
            "cùng HĐ, cùng actor, đổi ĐÚNG MỘT biến (upload thêm 1 file purpose=3) ⇒ badge lật true. "
            + "Nếu vẫn false thì mấy assert âm ở trên xanh vì cờ chết cứng, không phải vì lọc đúng");

        // ---- Khối B — GHIM CAVEAT 1 (lane BE `sub-implementer-backend-0.md` §8 điểm 2):
        //      cờ KHÔNG xét phase. HĐ `ChoDuyet` có purpose=3 vẫn `true`. Đây là hành vi CỐ Ý
        //      hiện tại (FE tự lọc `phase=9`), không phải bug — ghim để lần sau ai đổi thì
        //      biết mình đang ĐỔI HỢP ĐỒNG chứ không phải sửa vặt.
        var cPending = await AddContractAsync(db, sup, proj, drafter.Id, type, ContractPhase.ChoDuyet, "HĐ chờ duyệt mà đã có scan");
        await AddAttachmentAsync(db, cPending, AttachmentPurpose.SealedCopy);
        (await ListAsync(db, admin)).Items.Single(i => i.Id == cPending.Id).HasSealedCopy.Should().BeTrue(
            "cờ derive THUẦN theo purpose, không kèm điều kiện phase (`ContractFeatures.cs:355`) ⇒ "
            + "trách nhiệm lọc `phase=9` nằm ở phía gọi (7 leaf `Hdc_*`)");

        // ---- Khối C — GHIM CAVEAT 2 (§8 điểm 1 + `ContractDtos.cs:18-25`): cờ CHỈ được
        //      derive ở màn danh sách. Màn "Đã xóa" và "Hộp thư" dựng DTO bằng 12 arg nên
        //      nhận default `false` — dù HĐ ĐANG CÓ đính kèm purpose=3. Test theo CODE.
        cSealed.IsDeleted = true;            // KHÔNG `Remove()`: fixture không wire interceptor ⇒ Remove = xoá CỨNG (S155)
        cSealed.DeletedAt = Clock2026;
        await db.SaveChangesAsync(CancellationToken.None);

        (await db.ContractAttachments.CountAsync(a => a.ContractId == cSealed.Id)).Should().Be(1,
            "đính kèm vẫn còn nguyên (xoá mềm HĐ không đụng bảng đính kèm) — nếu nó biến mất thì "
            + "assert dưới xanh vì lý do khác hẳn");
        (await ListDeletedAsync(db, admin)).Items.Single(i => i.Id == cSealed.Id).HasSealedCopy
            .Should().BeFalse(
                "màn 'Đã xóa' CỐ Ý không derive cờ (DTO default `= false`, `ContractDtos.cs:25`) dù HĐ "
                + "vẫn còn đính kèm purpose=3. Đây là ranh giới scope W7 đang được ghim, không phải "
                + "phép đo nói rằng HĐ đó không có bản cứng");

        (await InboxAsync(db, admin)).Single(i => i.Id == cPending.Id).HasSealedCopy.Should().BeFalse(
            "cùng lý do ở 'Hộp thư' — HĐ `cPending` vừa được chứng minh là `true` ở màn danh sách, "
            + "nên `false` ở đây đúng là do KHÔNG derive, không phải do HĐ khác");
    }
}
