using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Exceptions;   // ConflictException (T5/T6 guards)
using SolutionErp.Application.PurchaseEvaluations;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Contracts;   // ApprovalDecision enum (shared HĐ/PE)
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master;
using SolutionErp.Domain.Master.Catalogs;
using SolutionErp.Domain.PurchaseEvaluations;
using SolutionErp.Infrastructure.Services;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Services;

// ===== S133 (2026-07-17) T0 REPRODUCE — bug "phiếu DaDuyet đổi số ngân sách theo bảng LIVE" =====
// UAT anh Kiệt: "các phiếu đã duyệt thì KHÔNG cho thay đổi gì hết". Hiện tại mọi phiếu cùng cặp
// (ProjectId × WorkItemId) đọc CHUNG 1 record PeWorkItemBudgets LIVE:
//   - GetPurchaseEvaluationQueryHandler (PurchaseEvaluationFeatures.cs:911-912) load pairRec LIVE,
//     KHÔNG phân biệt Phase → :964-972 build PeBudgetSummaryDto từ pairRec.
//   ⇒ ai đó nhập lại ngân sách cho gói thầu (record dùng chung) thì phiếu ĐÃ DUYỆT cũng đổi số.
//
// TEST-BEFORE (bug fix = reproduce → fix, docs/rules.md §7). Test này PHẢI **RED** trên code hiện
// tại (đọc live → 999tr/888tr) và sẽ GREEN sau khi fix snapshot ngân sách tại thời điểm chuyển
// DaDuyet. KHÔNG touch production (src/Backend, fe-*), KHÔNG sửa test cũ — chỉ WRITE test này.
//
// ⚠️ Tiền đề T0 CHUẨN (MEMORY-SLICE): đưa phiếu tới DaDuyet QUA service finalize path (mirror
// PeApproverFinalizeTests test #5 AtLastSlot: workflow V2 1 Bước 1 Cấp AllowApproverFinalize=true,
// approver duyệt + giá chốt → TỰ DaDuyet). TUYỆT ĐỐI KHÔNG seed thẳng Phase=DaDuyet — vì sau fix,
// snapshot chỉ được set qua transition; seed thẳng sẽ làm T0 mất nghĩa (không có bản chụp để giữ).
public class PeBudgetFreezeTests
{
    // Giá chốt hợp lệ khi finalize (ApplyApprovedPriceOnFinalize đòi source ∈ ValidApprovedPriceSources).
    private const decimal ValidApprovedPrice = 400_000_000m;
    private const string ValidApprovedSource = "Ncc";

    // Bộ số ngân sách TẠI thời điểm duyệt (snapshot phải giữ) vs SAU khi duyệt (record live bị đổi).
    private const decimal ProAtApproval = 100_000_000m;   // PRO ban hành lúc duyệt (kỳ vọng giữ)
    private const decimal CcmAtApproval = 200_000_000m;   // CCM ban hành lúc duyệt (kỳ vọng giữ)
    private const decimal ProAfterApproval = 999_000_000m; // PRO đổi SAU khi duyệt (record live)
    private const decimal CcmAfterApproval = 888_000_000m; // CCM đổi SAU khi duyệt (record live)

    private static (PurchaseEvaluationWorkflowService svc, IdentityFixture fix,
        TestApplicationDbContext db) CreateService()
    {
        var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var um = fix.Services.GetRequiredService<UserManager<User>>();
        var clock = new FixedDateTime(new DateTime(2026, 7, 17, 0, 0, 0, DateTimeKind.Utc));
        var notify = new NoOpNotificationService();
        var svc = new PurchaseEvaluationWorkflowService(db, clock, notify, um);
        return (svc, fix, db);
    }

    // Workflow V2 1 Bước 1 Cấp, ApproverUserId = approver, AllowApproverFinalize=true → duyệt tới
    // cấp này (slot cuối) = TỰ DaDuyet qua finalize-branch (PurchaseEvaluationWorkflowService.cs:855).
    private static async Task<ApprovalWorkflow> SeedFinalizeWorkflowAsync(
        TestApplicationDbContext db, Guid approverUserId, string code = "QT-BF-V2")
    {
        var wf = new ApprovalWorkflow
        {
            Code = code,   // unique per call — UNIQUE(Code,Version) khi 1 test seed nhiều workflow
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            Name = "QT test budget-freeze",
            IsActive = true,
            IsUserSelectable = true,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflowId = wf.Id,
            Order = 1,
            Name = "Bước 1",
        };
        step.Levels.Add(new ApprovalWorkflowLevel
        {
            ApprovalWorkflowStepId = step.Id,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approverUserId,
            AllowApproverFinalize = true,   // slot cuối = KẾT THÚC → duyệt là DaDuyet
        });
        wf.Steps.Add(step);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync(CancellationToken.None);
        return wf;
    }

    // =====================================================================
    // T0 REPRODUCE — phiếu DaDuyet PHẢI giữ ngân sách tại thời điểm duyệt (snapshot),
    // KHÔNG chạy theo record PeWorkItemBudgets live bị đổi sau khi duyệt.
    //   RED hiện tại: budgetSummary đọc live → 999tr / 888tr.
    //   GREEN sau fix: snapshot → 100tr / 200tr.
    // =====================================================================
    [Fact]
    public async Task DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            // (a) Cặp (Dự án × Hạng mục) + record ngân sách gói thầu PRO=100tr / CCM=200tr.
            var project = new Project { Id = Guid.NewGuid(), Code = "PRJ-BF", Name = "Dự án budget-freeze" };
            var wi = new WorkItem { Id = Guid.NewGuid(), Code = "WI-BF", Name = "Hạng mục budget-freeze", IsActive = true };
            db.Projects.Add(project);
            db.WorkItems.Add(wi);
            db.PeWorkItemBudgets.Add(new PeWorkItemBudget
            {
                Id = Guid.NewGuid(),
                ProjectId = project.Id,
                WorkItemId = wi.Id,
                ProInitialAmount = ProAtApproval,   // 100tr (PRO ban hành lúc duyệt)
                InitialAmount = CcmAtApproval,       // 200tr (CCM ban hành lúc duyệt)
            });
            await db.SaveChangesAsync(CancellationToken.None);

            // approver = người duyệt cấp finalize; drafter = "user thường" xem phiếu sau khi duyệt.
            var approver = (await fix.CreateUserAsync("appr@bf.test", "Approver", null, Array.Empty<string>())).Id;
            var drafter = (await fix.CreateUserAsync("drafter@bf.test", "Drafter", null, Array.Empty<string>())).Id;

            var wf = await SeedFinalizeWorkflowAsync(db, approver);

            // (b) Phiếu tại slot duyệt cuối (ChoDuyet) gắn ĐÚNG cặp + workflow finalize.
            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-BF-001",
                TenGoiThau = "Gói thầu budget-freeze",
                ProjectId = project.Id,
                WorkItemId = wi.Id,
                DrafterUserId = drafter,
                ApprovalWorkflowId = wf.Id,
                CurrentWorkflowStepIndex = 0,
                CurrentApprovalLevelOrder = 1,
                SlaDeadline = new DateTime(2026, 7, 24, 0, 0, 0, DateTimeKind.Utc),
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            // (b') Đưa tới DaDuyet QUA service finalize path (KHÔNG seed thẳng Phase=DaDuyet).
            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.ChoDuyet,   // approve-in-place
                actorUserId: approver,
                actorRoles: Array.Empty<string>(),
                decision: ApprovalDecision.Approve,
                comment: null,
                approvedPriceAmount: ValidApprovedPrice,
                approvedPriceSource: ValidApprovedSource,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet,
                "tiền đề T0: phiếu PHẢI ở DaDuyet qua finalize path (mirror PeApproverFinalizeTests), không seed thẳng");

            // (c) SAU khi duyệt, đổi record ngân sách LIVE: PRO 100tr→999tr, CCM 200tr→888tr (Admin).
            //     UpdatePeBudgetPro/Ccm KHÔNG có phase-guard → chạy được ở DaDuyet (đây chính là bug:
            //     pair dùng chung bị đổi → phiếu đã duyệt cũng đổi số).
            var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin);
            await new UpdatePeBudgetProCommandHandler(db, admin)
                .Handle(new UpdatePeBudgetProCommand(pe.Id, ProAfterApproval, null, "đổi sau khi duyệt"),
                    CancellationToken.None);
            await new UpdatePeBudgetCcmCommandHandler(db, admin)
                .Handle(new UpdatePeBudgetCcmCommand(pe.Id, CcmAfterApproval, null, "đổi sau khi duyệt"),
                    CancellationToken.None);

            // Sanity: record LIVE thật sự đã đổi (chứng minh update ăn — assert (d) không vacuous-pass).
            var liveRec = await db.PeWorkItemBudgets.AsNoTracking()
                .SingleAsync(b => b.ProjectId == project.Id && b.WorkItemId == wi.Id);
            liveRec.ProInitialAmount.Should().Be(ProAfterApproval, "record LIVE đã đổi PRO → 999tr");
            liveRec.InitialAmount.Should().Be(CcmAfterApproval, "record LIVE đã đổi CCM → 888tr");

            // (d) "user thường" (drafter, isDrafter=true qua authz) xem phiếu DaDuyet → budgetSummary
            //     PHẢI GIỮ số tại thời điểm duyệt (snapshot), KHÔNG chạy theo record live mới đổi.
            var querier = new TestCurrentUser(drafter);   // no roles = user thường
            var handler = new GetPurchaseEvaluationQueryHandler(
                db, fix.Services.GetRequiredService<UserManager<User>>(), querier);
            var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None);

            bundle.BudgetSummary.Should().NotBeNull("phiếu có WorkItemId → budgetSummary được build");
            var s = bundle.BudgetSummary!;

            // ⭐ RED trên code hiện tại (đọc live → 999tr / 888tr). GREEN sau fix snapshot (100tr / 200tr).
            s.ProInitialAmount.Should().Be(ProAtApproval,
                "phiếu DaDuyet phải GIỮ ngân sách PRO tại thời điểm duyệt (100tr), KHÔNG chạy theo record live (999tr)");
            s.InitialAmount.Should().Be(CcmAtApproval,
                "phiếu DaDuyet phải GIỮ ngân sách CCM tại thời điểm duyệt (200tr), KHÔNG chạy theo record live (888tr)");
        }
    }

    // =====================================================================
    // ===== INV-C (S133) T1-T9 — phủ 4 nhánh snapshot + 2 guard + edge =====
    // BE ĐÃ LAND (11 cột ApprovedBudget* + helper ApplyBudgetSnapshotOnFinalizeAsync 4 site
    // + display-gate frozen/live + 2 terminal-guard). T1-T9 GREEN ngay (KHÔNG RED phase —
    // T0 là reproduce duy nhất). Test theo CODE (S34 single-source-of-truth). KHÔNG sửa T0.
    // =====================================================================

    // Bộ số ngân sách gói thầu TẠI thời điểm duyệt (snapshot phải giữ nguyên bộ này).
    private const decimal PairProInitial = 100_000_000m;   // pair.ProInitialAmount (PRO)
    private const decimal PairProAdjust = -10_000_000m;    // pair.ProAdjustmentAmount (PRO, ÂM hợp lệ)
    private const string PairProNote = "PRO ban hành Q3 — dự trù đơn giá";
    private const decimal PairCcmInitial = 200_000_000m;   // pair.InitialAmount (CCM)
    private const decimal PairCcmAdjust = -20_000_000m;    // pair.AdjustmentAmount (CCM, ÂM hợp lệ)
    private const string PairCcmNote = "CCM nguồn dự toán đã duyệt";

    // Clock của CreateService (mirror T0) — SnapshotAt kỳ vọng = giá trị này.
    private static readonly DateTime ClockNow = new(2026, 7, 17, 0, 0, 0, DateTimeKind.Utc);

    // ---- shared seed helpers (INV-C) ----

    private static async Task<(Project project, WorkItem wi)> SeedProjectAndWorkItemAsync(
        TestApplicationDbContext db, string suffix)
    {
        var project = new Project { Id = Guid.NewGuid(), Code = "PRJ-BF-" + suffix, Name = "Dự án " + suffix };
        var wi = new WorkItem { Id = Guid.NewGuid(), Code = "WI-BF-" + suffix, Name = "Hạng mục " + suffix, IsActive = true };
        db.Projects.Add(project);
        db.WorkItems.Add(wi);
        await db.SaveChangesAsync(CancellationToken.None);
        return (project, wi);
    }

    // 1 record PeWorkItemBudget per cặp (ProjectId × WorkItemId). NAMING: Initial/Adjustment = CCM.
    private static async Task SeedPairAsync(
        TestApplicationDbContext db, Guid projectId, Guid wiId,
        decimal? proInit, decimal? proAdj, string? proNote,
        decimal? ccmInit, decimal? ccmAdj, string? ccmNote)
    {
        db.PeWorkItemBudgets.Add(new PeWorkItemBudget
        {
            Id = Guid.NewGuid(),
            ProjectId = projectId,
            WorkItemId = wiId,
            ProInitialAmount = proInit,
            ProAdjustmentAmount = proAdj,
            ProNote = proNote,
            InitialAmount = ccmInit,
            AdjustmentAmount = ccmAdj,
            CcmNote = ccmNote,
        });
        await db.SaveChangesAsync(CancellationToken.None);
    }

    // PE đứng tại slot duyệt cuối V2 (ChoDuyet) gắn cặp + workflow đã pin.
    private static PurchaseEvaluation BuildPeAtFinalizeSlot(
        Guid approvalWorkflowId, Guid projectId, Guid? workItemId, Guid drafterUserId, string code)
        => new()
        {
            Id = Guid.NewGuid(),
            Type = PurchaseEvaluationType.DuyetNcc,
            Phase = PurchaseEvaluationPhase.ChoDuyet,
            MaPhieu = code,
            TenGoiThau = "Gói thầu " + code,
            ProjectId = projectId,
            WorkItemId = workItemId,
            DrafterUserId = drafterUserId,
            ApprovalWorkflowId = approvalWorkflowId,
            CurrentWorkflowStepIndex = 0,
            CurrentApprovalLevelOrder = 1,
            SlaDeadline = new DateTime(2026, 7, 24, 0, 0, 0, DateTimeKind.Utc),
        };

    // Approve-in-place + giá chốt hợp lệ (mọi nhánh finalize human cần giá chốt).
    private static Task ApproveWithPriceAsync(
        PurchaseEvaluationWorkflowService svc, PurchaseEvaluation pe, Guid approver, string[] roles) =>
        svc.TransitionAsync(
            evaluation: pe,
            targetPhase: PurchaseEvaluationPhase.ChoDuyet,
            actorUserId: approver,
            actorRoles: roles,
            decision: ApprovalDecision.Approve,
            comment: null,
            approvedPriceAmount: ValidApprovedPrice,
            approvedPriceSource: ValidApprovedSource,
            ct: CancellationToken.None);

    // Seed 1 đơn vị IsWinner + 1 detail + 1 quote IsSelected cho phiếu (own quotes → winnerQuoteTotal).
    private static async Task SeedWinnerQuoteAsync(
        TestApplicationDbContext db, PurchaseEvaluation pe, decimal thanhTien)
    {
        var pes = new PurchaseEvaluationSupplier
        {
            Id = Guid.NewGuid(),
            PurchaseEvaluationId = pe.Id,
            SupplierId = Guid.NewGuid(),
            Order = 0,
            IsWinner = true,
        };
        var detail = new PurchaseEvaluationDetail
        {
            Id = Guid.NewGuid(),
            PurchaseEvaluationId = pe.Id,
            GroupCode = "A.I",
            GroupName = "Bê tông",
            NoiDung = "Concrete",
            Order = 0,
        };
        db.PurchaseEvaluationSuppliers.Add(pes);
        db.PurchaseEvaluationDetails.Add(detail);
        db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
        {
            Id = Guid.NewGuid(),
            PurchaseEvaluationDetailId = detail.Id,
            PurchaseEvaluationSupplierId = pes.Id,
            ThanhTien = thanhTien,
            IsSelected = true,
        });
        await db.SaveChangesAsync(CancellationToken.None);
    }

    // Peer PE cùng cặp cho accumulator (lũy kế phiếu TRƯỚC). CreatedAt override thủ công
    // (TestApplicationDbContext KHÔNG wire audit interceptor) để < CreatedAt phiếu đang xét.
    private static async Task SeedPeerAsync(
        TestApplicationDbContext db, Guid projectId, Guid wiId,
        PurchaseEvaluationPhase phase, decimal? budgetPeriod, DateTime createdAt,
        decimal? winnerQuote, string code)
    {
        var peer = new PurchaseEvaluation
        {
            Id = Guid.NewGuid(),
            Type = PurchaseEvaluationType.DuyetNcc,
            Phase = phase,
            MaPhieu = code,
            TenGoiThau = "Peer " + code,
            ProjectId = projectId,
            WorkItemId = wiId,
            DrafterUserId = Guid.NewGuid(),
            BudgetPeriodAmount = budgetPeriod,
        };
        db.PurchaseEvaluations.Add(peer);
        await db.SaveChangesAsync(CancellationToken.None);
        peer.CreatedAt = createdAt;   // accumulator dùng CreatedAt < this
        await db.SaveChangesAsync(CancellationToken.None);
        if (winnerQuote is decimal wq)
            await SeedWinnerQuoteAsync(db, peer, wq);
    }

    // Workflow V2 1 Bước 1 Cấp KHÔNG finalize-flag KHÔNG threshold → approve slot cuối =
    // nhánh all-steps-done (:933). Khác SeedFinalizeWorkflowAsync (AllowApproverFinalize=true, :861).
    private static async Task<ApprovalWorkflow> SeedPlainWorkflowAsync(
        TestApplicationDbContext db, Guid approverUserId)
    {
        var wf = new ApprovalWorkflow
        {
            Code = "QT-BF-PLAIN",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            Name = "QT budget-freeze all-steps",
            IsActive = true,
            IsUserSelectable = true,
        };
        var step = new ApprovalWorkflowStep { ApprovalWorkflowId = wf.Id, Order = 1, Name = "Bước 1" };
        step.Levels.Add(new ApprovalWorkflowLevel
        {
            ApprovalWorkflowStepId = step.Id,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approverUserId,
        });
        wf.Steps.Add(step);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync(CancellationToken.None);
        return wf;
    }

    // Workflow V2 1 Bước 1 Cấp CCM + CeoApprovalThreshold → CCM tích uỷ-quyền finalize
    // (nhánh :900). Cấp KHÔNG AllowApproverFinalize (mặc định) → rơi xuống ccm-delegation.
    private static async Task<ApprovalWorkflow> SeedCcmThresholdWorkflowAsync(
        TestApplicationDbContext db, Guid ccmUserId, decimal ceoThreshold)
    {
        var wf = new ApprovalWorkflow
        {
            Code = "QT-BF-CCM",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            Name = "QT budget-freeze CCM threshold",
            IsActive = true,
            IsUserSelectable = true,
            CeoApprovalThreshold = ceoThreshold,
        };
        var step = new ApprovalWorkflowStep { ApprovalWorkflowId = wf.Id, Order = 1, Name = "Bước 1" };
        step.Levels.Add(new ApprovalWorkflowLevel
        {
            ApprovalWorkflowStepId = step.Id,
            Order = 1,
            Name = "Cấp 1 (CCM)",
            ApproverUserId = ccmUserId,
        });
        wf.Steps.Add(step);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync(CancellationToken.None);
        return wf;
    }

    // Seed cặp (full pair 6 cột) + workflow level-finalize + PE ChoDuyet → finalize qua service →
    // trả PE ĐÃ DaDuyet (snapshot đã chốt). Reuse cho T5/T6/T7(frozen). withWorkItem/seedPair tuỳ chọn.
    private static async Task<(PurchaseEvaluation pe, Guid drafter, Project project, WorkItem wi)>
        SeedAndFinalizeAsync(
            PurchaseEvaluationWorkflowService svc, IdentityFixture fix, TestApplicationDbContext db,
            string suffix, bool withWorkItem = true, bool seedPair = true)
    {
        var (project, wi) = await SeedProjectAndWorkItemAsync(db, suffix);
        Guid? wiId = withWorkItem ? wi.Id : null;
        if (seedPair && withWorkItem)
            await SeedPairAsync(db, project.Id, wi.Id,
                PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote);

        var approver = (await fix.CreateUserAsync($"appr-{suffix}@bf.test", "Approver", null, Array.Empty<string>())).Id;
        var drafter = (await fix.CreateUserAsync($"drafter-{suffix}@bf.test", "Drafter", null, Array.Empty<string>())).Id;
        var wf = await SeedFinalizeWorkflowAsync(db, approver, $"QT-BF-V2-{suffix}");

        var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wiId, drafter, $"PE-BF-{suffix}");
        db.PurchaseEvaluations.Add(pe);
        await db.SaveChangesAsync(CancellationToken.None);
        await ApproveWithPriceAsync(svc, pe, approver, Array.Empty<string>());
        pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "tiền đề helper: PE phải ở DaDuyet qua finalize path");
        return (pe, drafter, project, wi);
    }

    // =====================================================================
    // T1 — LEVEL-FINALIZE (:861) chốt ĐỦ 11 cột snapshot: 6 cột pair (PRO/CCM Initial+
    //   Adjustment+Note) + 4 cột lũy kế (accumulator, theo 2 peer seed) + SnapshotAt.
    //   (T0 chỉ assert 2 số — T1 phủ toàn bộ bộ chụp.)
    // =====================================================================
    [Fact]
    public async Task LevelFinalize_SnapshotsAll11Columns_FromPairAndAccumulator()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T1");
            await SeedPairAsync(db, project.Id, wi.Id,
                PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote);

            // 2 peer cùng cặp (CreatedAt sớm hơn) → lũy kế xác định:
            //   peer DaDuyet: budget 100tr (PrevSubmitted) + winner quote 90tr (PrevSelected).
            //   peer ChoDuyet: budget 50tr (PrevSubmitted, KHÔNG PrevSelected).
            var baseT = new DateTime(2026, 7, 17, 8, 0, 0, DateTimeKind.Utc);
            await SeedPeerAsync(db, project.Id, wi.Id, PurchaseEvaluationPhase.DaDuyet,
                budgetPeriod: 100_000_000m, createdAt: baseT.AddDays(-3), winnerQuote: 90_000_000m, code: "PE-BF-T1-DD");
            await SeedPeerAsync(db, project.Id, wi.Id, PurchaseEvaluationPhase.ChoDuyet,
                budgetPeriod: 50_000_000m, createdAt: baseT.AddDays(-2), winnerQuote: null, code: "PE-BF-T1-CD");

            var approver = (await fix.CreateUserAsync("appr@t1.test", "Approver", null, Array.Empty<string>())).Id;
            var drafter = (await fix.CreateUserAsync("drafter@t1.test", "Drafter", null, Array.Empty<string>())).Id;
            var wf = await SeedFinalizeWorkflowAsync(db, approver);

            var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, drafter, "PE-BF-T1");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            pe.CreatedAt = baseT;   // muộn hơn 2 peer → cả 2 lọt lũy kế
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveWithPriceAsync(svc, pe, approver, Array.Empty<string>());
            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet);

            var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id);
            // 6 cột pair (NAMING: Pro* = PRO; Ccm* = CCM).
            s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial, "chốt PRO ban hành từ pair");
            s.ApprovedBudgetProAdjustmentAmount.Should().Be(PairProAdjust, "chốt PRO hiệu chỉnh (ÂM) từ pair");
            s.ApprovedBudgetProNote.Should().Be(PairProNote, "chốt ghi chú PRO từ pair");
            s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial, "chốt CCM ban hành từ pair.InitialAmount");
            s.ApprovedBudgetCcmAdjustmentAmount.Should().Be(PairCcmAdjust, "chốt CCM hiệu chỉnh (ÂM) từ pair.AdjustmentAmount");
            s.ApprovedBudgetCcmNote.Should().Be(PairCcmNote, "chốt ghi chú CCM từ pair.CcmNote");
            // 4 cột lũy kế từ accumulator.
            s.ApprovedBudgetPrevSubmittedTotal.Should().Be(150_000_000m, "peer DaDuyet 100tr + ChoDuyet 50tr");
            s.ApprovedBudgetPrevSubmittedCount.Should().Be(2);
            s.ApprovedBudgetPrevSelectedTotal.Should().Be(90_000_000m, "chỉ peer DaDuyet có winner quote");
            s.ApprovedBudgetPrevSelectedCount.Should().Be(1);
            // Cờ đã-chốt.
            s.ApprovedBudgetSnapshotAt.Should().NotBeNull("SnapshotAt LUÔN set khi finalize = cờ đã chốt");
        }
    }

    // =====================================================================
    // T2 — CCM-THRESHOLD delegation (:900): CCM tích uỷ-quyền + gói < ngưỡng CEO →
    //   DaDuyet qua nhánh delegation → snapshot ngân sách set (pair cols + SnapshotAt).
    // =====================================================================
    [Fact]
    public async Task CcmThresholdFinalize_SetsBudgetSnapshot()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T2");
            await SeedPairAsync(db, project.Id, wi.Id,
                PairProInitial, null, null, PairCcmInitial, null, null);

            var ccm = (await fix.CreateUserAsync("ccm@t2.test", "CCM", null, new[] { AppRoles.CostControl })).Id;
            var wf = await SeedCcmThresholdWorkflowAsync(db, ccm, ceoThreshold: 1_000_000_000m);

            var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, Guid.NewGuid(), "PE-BF-T2");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Gói 400tr < ngưỡng 1 tỷ (winnerQuoteTotal = SUM quote IsSelected của phiếu).
            await SeedWinnerQuoteAsync(db, pe, ValidApprovedPrice);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.ChoDuyet,
                actorUserId: ccm,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Approve,
                comment: null,
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice,
                approvedPriceSource: ValidApprovedSource,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "CCM uỷ-quyền + gói < ngưỡng → DaDuyet (nhánh delegation)");

            var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id);
            s.ApprovedBudgetSnapshotAt.Should().NotBeNull("nhánh CCM-threshold PHẢI chốt snapshot");
            s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial, "chốt PRO từ pair tại nhánh delegation");
            s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial, "chốt CCM từ pair tại nhánh delegation");
        }
    }

    // =====================================================================
    // T3 — ALL-STEPS-DONE (:933): 1 Bước 1 Cấp KHÔNG finalize-flag → approve slot cuối →
    //   terminal DaDuyet nhánh all-steps → snapshot set.
    // =====================================================================
    [Fact]
    public async Task AllStepsDoneFinalize_SetsBudgetSnapshot()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T3");
            await SeedPairAsync(db, project.Id, wi.Id,
                PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote);

            var approver = (await fix.CreateUserAsync("appr@t3.test", "Approver", null, Array.Empty<string>())).Id;
            var wf = await SeedPlainWorkflowAsync(db, approver);   // AllowApproverFinalize=false

            var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, Guid.NewGuid(), "PE-BF-T3");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveWithPriceAsync(svc, pe, approver, Array.Empty<string>());

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "slot cuối, hết bước → terminal DaDuyet (all-steps)");
            var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id);
            s.ApprovedBudgetSnapshotAt.Should().NotBeNull("nhánh all-steps-done PHẢI chốt snapshot");
            s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial);
            s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial);
        }
    }

    // =====================================================================
    // T4 — V1 LEGACY (:1108): phiếu pin WorkflowDefinitionId (V1 flat) đi hết steps →
    //   nhánh V1 → snapshot set NHƯNG ApprovedPriceAmount KHÔNG set (V1 không có luồng giá-chốt).
    // =====================================================================
    [Fact]
    public async Task V1LegacyFinalize_SetsBudgetSnapshot_WithoutApprovedPrice()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T4");
            await SeedPairAsync(db, project.Id, wi.Id,
                PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote);

            var approver = (await fix.CreateUserAsync("appr@t4.test", "Approver V1", null, Array.Empty<string>())).Id;

            // V1 workflow definition: 1 step, approver = explicit User (match qua userManager).
            var def = new PurchaseEvaluationWorkflowDefinition
            {
                Id = Guid.NewGuid(),
                Code = "PE-V1-BF",
                Version = 1,
                EvaluationType = PurchaseEvaluationType.DuyetNcc,
                Name = "V1 legacy budget-freeze",
                IsActive = true,
            };
            var step = new PurchaseEvaluationWorkflowStep
            {
                Id = Guid.NewGuid(),
                PurchaseEvaluationWorkflowDefinitionId = def.Id,
                Order = 1,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                Name = "Bước 1 (V1)",
            };
            step.Approvers.Add(new PurchaseEvaluationWorkflowStepApprover
            {
                Id = Guid.NewGuid(),
                PurchaseEvaluationWorkflowStepId = step.Id,
                Kind = WorkflowApproverKind.User,
                AssignmentValue = approver.ToString(),
            });
            def.Steps.Add(step);
            db.PurchaseEvaluationWorkflowDefinitions.Add(def);
            await db.SaveChangesAsync(CancellationToken.None);

            // PE pin V1 (WorkflowDefinitionId) — ApprovalWorkflowId=null → route ApproveV1LegacyAsync.
            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-BF-T4",
                TenGoiThau = "Gói thầu V1 legacy",
                ProjectId = project.Id,
                WorkItemId = wi.Id,
                DrafterUserId = Guid.NewGuid(),
                WorkflowDefinitionId = def.Id,
                ApprovalWorkflowId = null,
                CurrentWorkflowStepIndex = 0,
                SlaDeadline = new DateTime(2026, 7, 24, 0, 0, 0, DateTimeKind.Utc),
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            // V1 KHÔNG có luồng giá-chốt → KHÔNG truyền approvedPrice.
            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.ChoDuyet,
                actorUserId: approver,
                actorRoles: Array.Empty<string>(),
                decision: ApprovalDecision.Approve,
                comment: null,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "V1 legacy hết step → DaDuyet (:1108)");
            var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id);
            s.ApprovedBudgetSnapshotAt.Should().NotBeNull("V1 legacy PHẢI chốt snapshot (nhánh :1108)");
            s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial, "V1 vẫn chốt số ngân sách từ pair");
            s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial);
            s.ApprovedPriceAmount.Should().BeNull("V1 legacy KHÔNG có luồng giá-chốt → ApprovedPriceAmount null");
        }
    }

    // =====================================================================
    // T5 — GUARD SetPeCcmBudgetPeriod: phiếu DaDuyet → CCM VÀ Admin đều ConflictException
    //   (guard đặt TRƯỚC role-gate → chặn cả 2). Phiếu ChoDuyet → CCM vẫn set được (guard không lạm).
    // =====================================================================
    [Fact]
    public async Task SetCcmBudgetPeriodGuard_BlocksDaDuyet_ForCcmAndAdmin_AllowsChoDuyet()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            // Phiếu DaDuyet THẬT qua finalize path.
            var (peDone, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T5done");

            var ccm = new TestCurrentUser(Guid.NewGuid(), "CCM", null, AppRoles.CostControl);
            var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin);

            var byCcm = async () => await new SetPeCcmBudgetPeriodCommandHandler(db, ccm)
                .Handle(new SetPeCcmBudgetPeriodCommand(peDone.Id, 123_000_000m), CancellationToken.None);
            await byCcm.Should().ThrowAsync<ConflictException>()
                .WithMessage("*đã kết thúc*");

            var byAdmin = async () => await new SetPeCcmBudgetPeriodCommandHandler(db, admin)
                .Handle(new SetPeCcmBudgetPeriodCommand(peDone.Id, 456_000_000m), CancellationToken.None);
            await byAdmin.Should().ThrowAsync<ConflictException>()
                .WithMessage("*đã kết thúc*", "guard chặn CẢ Admin (đặt TRƯỚC role-gate)");

            // Phiếu ChoDuyet (chưa duyệt) → CCM set BÌNH THƯỜNG (guard không lạm sang phiếu chưa kết thúc).
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T5open");
            var peOpen = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-BF-T5-OPEN",
                TenGoiThau = "Gói thầu chưa duyệt",
                ProjectId = project.Id,
                WorkItemId = wi.Id,
                DrafterUserId = Guid.NewGuid(),
            };
            db.PurchaseEvaluations.Add(peOpen);
            await db.SaveChangesAsync(CancellationToken.None);

            await new SetPeCcmBudgetPeriodCommandHandler(db, ccm)
                .Handle(new SetPeCcmBudgetPeriodCommand(peOpen.Id, 77_000_000m), CancellationToken.None);
            var reloaded = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == peOpen.Id);
            reloaded.CcmBudgetPeriodAmount.Should().Be(77_000_000m, "phiếu ChoDuyet → CCM vẫn nhập NS kỳ này được");
        }
    }

    // =====================================================================
    // T6 — GUARD AdjustBudget: phiếu DaDuyet (qua finalize) + Admin → Conflict; phiếu
    //   TuChoi (seed thẳng — SPEC cho phép vì guard chỉ đọc Phase) + Admin → Conflict.
    // =====================================================================
    [Fact]
    public async Task AdjustBudgetGuard_BlocksDaDuyetAndTuChoi_ForAdmin()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin);

            // (a) DaDuyet THẬT qua finalize → Admin adjust → Conflict.
            var (peDone, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T6done");
            var adjDone = async () => await new AdjustPurchaseEvaluationBudgetCommandHandler(db, admin)
                .Handle(new AdjustPurchaseEvaluationBudgetCommand(peDone.Id, 10_000_000m, null), CancellationToken.None);
            await adjDone.Should().ThrowAsync<ConflictException>()
                .WithMessage("*đã kết thúc*", "phiếu DaDuyet → chặn CẢ Admin (guard TRƯỚC isAdmin block)");

            // (b) TuChoi seed thẳng (không reachable qua service — S60 gỡ Từ chối). Guard chỉ đọc Phase.
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T6rej");
            var peReject = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.TuChoi,
                MaPhieu = "PE-BF-T6-REJ",
                TenGoiThau = "Gói thầu bị từ chối",
                ProjectId = project.Id,
                WorkItemId = wi.Id,
                DrafterUserId = Guid.NewGuid(),
            };
            db.PurchaseEvaluations.Add(peReject);
            await db.SaveChangesAsync(CancellationToken.None);

            var adjReject = async () => await new AdjustPurchaseEvaluationBudgetCommandHandler(db, admin)
                .Handle(new AdjustPurchaseEvaluationBudgetCommand(peReject.Id, 5_000_000m, null), CancellationToken.None);
            await adjReject.Should().ThrowAsync<ConflictException>()
                .WithMessage("*đã kết thúc*", "phiếu TuChoi cũng = kết thúc → ngân sách bất biến");
        }
    }

    // =====================================================================
    // T7 — DISPLAY: nhánh else-live (phiếu ChoDuyet) → số MỚI theo pair + BudgetFrozen=false
    //   + CanEdit theo role; nhánh frozen (phiếu DaDuyet) → CanEditPro=CanEditCcm=false (kể cả
    //   Admin) + BudgetFrozen=true.
    // =====================================================================
    [Fact]
    public async Task BudgetSummary_LiveVsFrozen_FlagsAndEditability()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var um = fix.Services.GetRequiredService<UserManager<User>>();

            // ---- Nhánh LIVE: phiếu ChoDuyet + pair, querier = Procurement + đồng thời drafter ----
            var (projectL, wiL) = await SeedProjectAndWorkItemAsync(db, "T7live");
            await SeedPairAsync(db, projectL.Id, wiL.Id, null, null, null, PairCcmInitial, null, null);
            var proUser = await fix.CreateUserAsync("pro@t7.test", "PRO", null, new[] { AppRoles.Procurement });
            var peLive = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-BF-T7-LIVE",
                TenGoiThau = "Gói thầu live",
                ProjectId = projectL.Id,
                WorkItemId = wiL.Id,
                DrafterUserId = proUser.Id,   // drafter → authz pass
            };
            db.PurchaseEvaluations.Add(peLive);
            await db.SaveChangesAsync(CancellationToken.None);

            var proQuerier = new TestCurrentUser(proUser.Id, "PRO", null, AppRoles.Procurement);
            var liveBundle = await new GetPurchaseEvaluationQueryHandler(db, um, proQuerier)
                .Handle(new GetPurchaseEvaluationQuery(peLive.Id), CancellationToken.None);
            var live = liveBundle.BudgetSummary!;
            live.BudgetFrozen.Should().BeFalse("phiếu ChoDuyet → nhánh live");
            live.InitialAmount.Should().Be(PairCcmInitial, "nhánh live đọc số MỚI từ record pair");
            live.CanEditPro.Should().BeTrue("querier role Procurement → được sửa PRO khi chưa duyệt");
            live.CanEditCcm.Should().BeFalse("querier KHÔNG có role CostControl → không sửa CCM");

            // ---- Nhánh FROZEN: phiếu DaDuyet (finalize) + querier Admin → vẫn KHÓA sửa ----
            var (peDone, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T7frozen");
            var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin);
            var frozenBundle = await new GetPurchaseEvaluationQueryHandler(db, um, admin)
                .Handle(new GetPurchaseEvaluationQuery(peDone.Id), CancellationToken.None);
            var frozen = frozenBundle.BudgetSummary!;
            frozen.BudgetFrozen.Should().BeTrue("phiếu DaDuyet + SnapshotAt != null → nhánh frozen");
            frozen.CanEditPro.Should().BeFalse("phiếu đã chốt → KHÔNG cho sửa PRO, kể cả Admin");
            frozen.CanEditCcm.Should().BeFalse("phiếu đã chốt → KHÔNG cho sửa CCM, kể cả Admin");
            frozen.InitialAmount.Should().Be(PairCcmInitial, "nhánh frozen serve CCM từ cột snapshot");
        }
    }

    // =====================================================================
    // T8 — NULL-SAFE: (a) phiếu WorkItemId null finalize → không NRE, bundle.BudgetSummary=null
    //   NHƯNG SnapshotAt VẪN set trên entity; (b) phiếu có WorkItemId nhưng KHÔNG có pair record →
    //   finalize OK, frozen bs non-null với 6 cột pair null.
    // =====================================================================
    [Fact]
    public async Task Finalize_NullSafe_NoWorkItemAndNoPairRecord()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var um = fix.Services.GetRequiredService<UserManager<User>>();
            var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin);

            // (a) WorkItemId null → helper set SnapshotAt nhưng bundle.BudgetSummary null.
            var (peNoWi, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T8nowi", withWorkItem: false, seedPair: false);
            var sNoWi = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == peNoWi.Id);
            sNoWi.ApprovedBudgetSnapshotAt.Should().NotBeNull("WorkItemId null vẫn set SnapshotAt (không NRE)");
            sNoWi.ApprovedBudgetProInitialAmount.Should().BeNull("WorkItemId null → không cặp → 10 cột null");
            var noWiBundle = await new GetPurchaseEvaluationQueryHandler(db, um, admin)
                .Handle(new GetPurchaseEvaluationQuery(peNoWi.Id), CancellationToken.None);
            noWiBundle.BudgetSummary.Should().BeNull("phiếu không gắn Hạng mục → BudgetSummary null");

            // (b) WorkItemId set nhưng KHÔNG seed pair → finalize OK, frozen bs non-null, 6 pair cols null.
            var (peNoPair, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T8nopair", withWorkItem: true, seedPair: false);
            var sNoPair = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == peNoPair.Id);
            sNoPair.ApprovedBudgetSnapshotAt.Should().NotBeNull("pair thiếu vẫn set SnapshotAt");
            sNoPair.ApprovedBudgetCcmInitialAmount.Should().BeNull("pair thiếu → 6 cột pair null");
            var noPairBundle = await new GetPurchaseEvaluationQueryHandler(db, um, admin)
                .Handle(new GetPurchaseEvaluationQuery(peNoPair.Id), CancellationToken.None);
            var bs = noPairBundle.BudgetSummary;
            bs.Should().NotBeNull("có WorkItemId → frozen bs vẫn build (dù pair rỗng)");
            bs!.BudgetFrozen.Should().BeTrue();
            bs.ProInitialAmount.Should().BeNull("pair thiếu → FE hiển thị '—' (cột PRO null)");
            bs.InitialAmount.Should().BeNull("pair thiếu → cột CCM null");
        }
    }

    // =====================================================================
    // T9 — RE-FINALIZE OVERWRITE: phiếu ĐÃ có cột snapshot CŨ (seed thẳng — SPEC cho phép để
    //   test semantics 'LUÔN overwrite') đi qua finalize → cột = giá trị pair HIỆN TẠI (mới),
    //   SnapshotAt refresh (không giữ giá trị cũ).
    // =====================================================================
    [Fact]
    public async Task ReFinalize_AlwaysOverwritesStaleSnapshot_RefreshesSnapshotAt()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T9");
            // Pair MỚI: ProInitial 500tr.
            await SeedPairAsync(db, project.Id, wi.Id, 500_000_000m, null, null, 600_000_000m, null, null);

            var approver = (await fix.CreateUserAsync("appr@t9.test", "Approver", null, Array.Empty<string>())).Id;
            var wf = await SeedFinalizeWorkflowAsync(db, approver);

            var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, Guid.NewGuid(), "PE-BF-T9");
            // Seed thẳng cột snapshot CŨ (stale) + SnapshotAt cũ để chứng minh helper LUÔN overwrite.
            var staleAt = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            pe.ApprovedBudgetProInitialAmount = 111_000_000m;   // giá trị cũ SAI
            pe.ApprovedBudgetCcmInitialAmount = 222_000_000m;
            pe.ApprovedBudgetSnapshotAt = staleAt;
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveWithPriceAsync(svc, pe, approver, Array.Empty<string>());
            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet);

            // Tracked entity: helper mutate in-place → overwrite bằng pair HIỆN TẠI.
            pe.ApprovedBudgetProInitialAmount.Should().Be(500_000_000m, "LUÔN overwrite: cột = pair mới (500tr), KHÔNG giữ 111tr cũ");
            pe.ApprovedBudgetCcmInitialAmount.Should().Be(600_000_000m, "LUÔN overwrite CCM: 600tr mới, KHÔNG giữ 222tr cũ");
            pe.ApprovedBudgetSnapshotAt.Should().Be(ClockNow, "SnapshotAt refresh về thời điểm duyệt (clock), KHÔNG giữ 2020");
            pe.ApprovedBudgetSnapshotAt.Should().NotBe(staleAt);

            // Persisted.
            var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id);
            s.ApprovedBudgetProInitialAmount.Should().Be(500_000_000m, "overwrite persist xuống DB");
        }
    }

    // =====================================================================
    // S133 site-5 (reviewer PASS_WITH_FIXES catch — #81 indirect writer): Admin manual-override
    // chuyển THẲNG → DaDuyet qua nhánh catch-all (`evaluation.Phase = targetPhase` — assignment
    // qua BIẾN, không phải token enum → literal-grep 4-site không thấy) cũng PHẢI chốt snapshot.
    // Thiếu → phiếu DaDuyet SnapshotAt=null → display-gate false → serve LIVE (mất bất biến).
    // =====================================================================
    [Fact]
    public async Task AdminOverride_DirectToDaDuyet_SetsBudgetSnapshot()
    {
        var (svc, fix, db) = CreateService();
        using (fix)
        {
            var (project, wi) = await SeedProjectAndWorkItemAsync(db, "AO");
            await SeedPairAsync(db, project.Id, wi.Id, ProAtApproval, null, null, CcmAtApproval, null, null);

            var admin = (await fix.CreateUserAsync("admin@ao.test", "Admin AO", null, new[] { AppRoles.Admin })).Id;

            // Phiếu ở DangSoanThao — KHÔNG rơi approve-step (fromPhase != ChoDuyet) → catch-all
            // admin-override khi actor là Admin.
            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.DangSoanThao,
                MaPhieu = "PE-BF-AO",
                TenGoiThau = "Gói thầu admin-override",
                ProjectId = project.Id,
                WorkItemId = wi.Id,
                DrafterUserId = admin,
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.DaDuyet,
                actorUserId: admin,
                actorRoles: new[] { AppRoles.Admin },
                decision: ApprovalDecision.Approve,
                comment: "admin override thẳng DaDuyet",
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "admin catch-all override phải ăn");
            pe.ApprovedBudgetSnapshotAt.Should().Be(ClockNow,
                "site-5: override thẳng → DaDuyet cũng phải chốt snapshot (RULE helper — reviewer S133)");
            pe.ApprovedBudgetProInitialAmount.Should().Be(ProAtApproval, "snapshot PRO từ pair lúc override");
            pe.ApprovedBudgetCcmInitialAmount.Should().Be(CcmAtApproval, "snapshot CCM từ pair lúc override");
        }
    }
}
