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

namespace SolutionErp.Infrastructure.Tests.Services;

// ===== FEATURE B (Mig 54 2026-06-18 anh Kiệt FDC) — CCM-finalize ĐỔI AUTO→OPT-IN =====
// PurchaseEvaluationWorkflowService.ApproveV2Async, finalize-branch line 830-867 prod
// (test theo CODE — S34 rule, KHÔNG touch production).
//
// ⚠️ SPEC CHANGE (Mig 54 vs S69):
//   TRƯỚC (S69): CCM duyệt + aw.CeoApprovalThreshold set + winnerQuoteTotal < ngưỡng
//     + chưa-slot-cuối → AUTO finalize DaDuyet (im lặng, KHÔNG cần tham số).
//   NAY (Mig 54): CHỈ finalize khi `finalizeByCcmDelegation=true` truyền vào
//     TransitionAsync VÀ đủ điều kiện (fail-closed, check theo thứ tự code line 832-851):
//       (1) aw.CeoApprovalThreshold != null  → null = ConflictException
//       (2) actorRoles chứa AppRoles.CostControl → khác = ForbiddenException
//       (3) winnerQuoteTotal < ngưỡng (STRICT `<`) → >= = ConflictException
//     Đủ 3 ĐK → ApplyApprovedPriceOnFinalize (BẮT BUỘC giá chốt cho human) → DaDuyet.
//   flag=false → bỏ qua finalize-branch hoàn toàn → advance tuyến tính lên CEO.
//
// ⚠️ finalize-branch (delegation) gọi ApplyApprovedPriceOnFinalize → human approver
//   PHẢI truyền approvedPriceAmount + approvedPriceSource hợp lệ, nếu không = Conflict
//   ("Chọn 1 giá chốt..."). Mọi case finalize dưới đây truyền giá hợp lệ.
//
// Mirror harness PeSubmitGuardAndBypassTests.cs cùng folder (IdentityFixture + SQLite
// + SeedWorkflowAsync + SeedWinnerWithQuoteAsync + reuse NoOpNotificationService internal).
// Tests này dựng PE TRỰC TIẾP ở ChoDuyet (đã qua submit guard) + pin pointer Step/Level
//   đứng tại slot CCM → drive 1 lần Approve.
public class PeCcmThresholdFinalizeTests
{
    private const decimal CeoThreshold = 1_000_000_000m;
    private const decimal ValidApprovedPrice = 500_000_000m;
    private const string ValidApprovedSource = "Ncc";

    private static (PurchaseEvaluationWorkflowService svc, IdentityFixture fix,
        TestApplicationDbContext db, FixedDateTime clock) 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, 6, 18, 0, 0, 0, DateTimeKind.Utc));
        var notify = new NoOpNotificationService();
        var svc = new PurchaseEvaluationWorkflowService(db, clock, notify, um);
        return (svc, fix, db, clock);
    }

    // PE đứng ở ChoDuyet tại pointer (stepIdx, levelOrder). SelectedSupplierId pin
    // winner. ApprovalWorkflowId pin V2 → ApproveV2Async branch.
    private static PurchaseEvaluation BuildPeAtApprovalSlot(
        Guid approvalWorkflowId,
        Guid? selectedSupplierId,
        int stepIdx,
        int levelOrder,
        string code = "PE-FB-001")
    {
        return new PurchaseEvaluation
        {
            Id = Guid.NewGuid(),
            Type = PurchaseEvaluationType.DuyetNcc,
            Phase = PurchaseEvaluationPhase.ChoDuyet,
            MaPhieu = code,
            TenGoiThau = "Test Feature B — CCM threshold finalize",
            ProjectId = Guid.NewGuid(),
            DrafterUserId = Guid.NewGuid(),
            ApprovalWorkflowId = approvalWorkflowId,
            SelectedSupplierId = selectedSupplierId,
            CurrentWorkflowStepIndex = stepIdx,
            CurrentApprovalLevelOrder = levelOrder,
            SlaDeadline = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc),
        };
    }

    // Seed 1 NCC tham gia (winner) + 1 detail + 1 quote ThanhTien=amount → winner
    // quote sum = amount. Return supplierId (master ref) để pin SelectedSupplierId.
    // [multi-NCC 2026-07-12 cardinality-change] ccm-delegate winnerQuoteTotal NAY = SUM
    // ThanhTien quote ĐƯỢC CHỌN (Quote.IsSelected — service :884-889 `where q.IsSelected`),
    // KHÔNG còn theo IsWinner supplier-row. Quote PHẢI IsSelected=true thì mới vào tổng
    // → boundary test (b)/(b2) (gói >= ngưỡng → Conflict) mới đúng; nếu IsSelected=false thì
    // tổng=0 luôn < ngưỡng → mất case Conflict. Set CẢ IsSelected + IsWinner (giữ invariant).
    private static async Task<Guid> SeedWinnerWithQuoteAsync(
        TestApplicationDbContext db, PurchaseEvaluation pe, decimal quoteThanhTien)
    {
        var supplierId = Guid.NewGuid();
        var pes = new PurchaseEvaluationSupplier
        {
            PurchaseEvaluationId = pe.Id,
            SupplierId = supplierId,
            Order = 0,
            IsWinner = true,   // derived == Any(quote.IsSelected)
        };
        var detail = new PurchaseEvaluationDetail
        {
            PurchaseEvaluationId = pe.Id,
            GroupCode = "A.I",
            GroupName = "Bê tông",
            NoiDung = "Concrete M100",
            Order = 0,
        };
        db.PurchaseEvaluationSuppliers.Add(pes);
        db.PurchaseEvaluationDetails.Add(detail);
        db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
        {
            PurchaseEvaluationDetailId = detail.Id,
            PurchaseEvaluationSupplierId = pes.Id,
            ThanhTien = quoteThanhTien,
            IsSelected = true,   // [multi-NCC] báo giá được chọn → vào winnerQuoteTotal
        });
        await db.SaveChangesAsync(CancellationToken.None);
        return supplierId;
    }

    // [multi-NCC 2026-07-12 — new test (c) threshold-crossing] Seed 1 winner-row với 2 quote:
    // 1 ĐƯỢC CHỌN (selectedThanhTien) + 1 KHÔNG chọn (unselectedThanhTien). Tổng-theo-IsSelected
    // = selectedThanhTien; tổng-theo-CŨ (whole-supplier IsWinner, gồm cả quote không chọn) =
    // selected + unselected. Return supplierId.
    private static async Task<Guid> SeedWinnerWithSelectedAndUnselectedQuoteAsync(
        TestApplicationDbContext db, PurchaseEvaluation pe,
        decimal selectedThanhTien, decimal unselectedThanhTien)
    {
        var supplierId = Guid.NewGuid();
        var pes = new PurchaseEvaluationSupplier
        {
            PurchaseEvaluationId = pe.Id, SupplierId = supplierId, Order = 0, IsWinner = true,
        };
        var detailSel = new PurchaseEvaluationDetail
        {
            PurchaseEvaluationId = pe.Id, GroupCode = "A.I", GroupName = "Bê tông",
            NoiDung = "Hạng mục ĐƯỢC chọn", Order = 0,
        };
        var detailUnsel = new PurchaseEvaluationDetail
        {
            PurchaseEvaluationId = pe.Id, GroupCode = "A.II", GroupName = "Thép",
            NoiDung = "Hạng mục KHÔNG chọn", Order = 1,
        };
        db.PurchaseEvaluationSuppliers.Add(pes);
        db.PurchaseEvaluationDetails.Add(detailSel);
        db.PurchaseEvaluationDetails.Add(detailUnsel);
        db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
        {
            PurchaseEvaluationDetailId = detailSel.Id, PurchaseEvaluationSupplierId = pes.Id,
            ThanhTien = selectedThanhTien, IsSelected = true,
        });
        db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
        {
            PurchaseEvaluationDetailId = detailUnsel.Id, PurchaseEvaluationSupplierId = pes.Id,
            ThanhTien = unselectedThanhTien, IsSelected = false,
        });
        await db.SaveChangesAsync(CancellationToken.None);
        return supplierId;
    }

    // Seed workflow V2 multi-step: stepApprovers[s] = mảng NV cho từng cấp (Order
    // 1-based) của bước s (Order s+1). 1 NV / cấp. ceoThreshold set CeoApprovalThreshold
    // (null = bỏ qua finalize). Return ApprovalWorkflow đã persist.
    private static async Task<ApprovalWorkflow> SeedWorkflowAsync(
        TestApplicationDbContext db, Guid[][] stepApprovers, decimal? ceoThreshold)
    {
        var wf = new ApprovalWorkflow
        {
            Code = "QT-FB-V2",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            Name = "QT test Feature B threshold",
            IsActive = true,
            IsUserSelectable = true,
            CeoApprovalThreshold = ceoThreshold,
        };
        for (int s = 0; s < stepApprovers.Length; s++)
        {
            var step = new ApprovalWorkflowStep
            {
                ApprovalWorkflowId = wf.Id,
                Order = s + 1,
                Name = $"Bước {s + 1}",
            };
            for (int lvl = 0; lvl < stepApprovers[s].Length; lvl++)
            {
                step.Levels.Add(new ApprovalWorkflowLevel
                {
                    ApprovalWorkflowStepId = step.Id,
                    Order = lvl + 1,
                    Name = $"Cấp {lvl + 1}",
                    ApproverUserId = stepApprovers[s][lvl],
                });
            }
            wf.Steps.Add(step);
        }
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync(CancellationToken.None);
        return wf;
    }

    // Approve-in-place (advance pointer). finalizeByCcmDelegation + giá chốt optional —
    // mặc định KHÔNG uỷ quyền + không giá (advance bình thường).
    private static Task ApproveAsync(
        PurchaseEvaluationWorkflowService svc, PurchaseEvaluation pe, Guid actorUserId,
        string[] roles,
        bool finalizeByCcmDelegation = false,
        decimal? approvedPriceAmount = null,
        string? approvedPriceSource = null) =>
        svc.TransitionAsync(
            evaluation: pe,
            targetPhase: PurchaseEvaluationPhase.ChoDuyet,  // approve-in-place (advance pointer)
            actorUserId: actorUserId,
            actorRoles: roles,
            decision: ApprovalDecision.Approve,
            comment: null,
            finalizeByCcmDelegation: finalizeByCcmDelegation,
            approvedPriceAmount: approvedPriceAmount,
            approvedPriceSource: approvedPriceSource,
            ct: CancellationToken.None);

    // =====================================================================
    // 1. ⭐ LOAD-BEARING — CCM tích uỷ-quyền (flag=true) + gói < ngưỡng + CCM
    //    mid-workflow (còn CEO sau) + truyền giá chốt → DaDuyet, bỏ CEO, pointers null.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmDelegationFlag_BelowThreshold_MidWorkflow_FinalizesDaDuyet_SkipsCeo_PointersCleared()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            // Workflow 2 bước: Bước 1 Cấp 1 = CCM (đang đứng đây), Bước 2 Cấp 1 = CEO.
            var ccm = (await fix.CreateUserAsync("ccm@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceo@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },   // Bước 1 Cấp 1 = CCM
                new[] { ceo },   // Bước 2 Cấp 1 = CEO (sẽ bị bỏ qua)
            }, ceoThreshold: CeoThreshold);

            // PE đứng tại Bước 1 (stepIdx 0) Cấp 1 — đến lượt CCM.
            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1);
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Gói 500tr < ngưỡng 1 tỷ.
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: ValidApprovedPrice);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            // ⭐ Phiếu DaDuyet ngay — KHÔNG advance sang Bước 2 (CEO bị bỏ qua).
            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet,
                "CCM tích uỷ-quyền + gói < ngưỡng CEO → finalize, bỏ CEO");
            pe.CurrentWorkflowStepIndex.Should().BeNull("terminal → step pointer null");
            pe.CurrentApprovalLevelOrder.Should().BeNull("terminal → level pointer null");
            pe.SlaDeadline.Should().BeNull("terminal → SLA null");
            // ① giá chốt phải được bind từ giá CCM truyền vào.
            pe.ApprovedPriceAmount.Should().Be(ValidApprovedPrice, "giá chốt bind khi finalize");
            pe.ApprovedPriceSource.Should().Be(ValidApprovedSource);

            // CEO KHÔNG được ghi opinion (bị bỏ qua hoàn toàn — chỉ CCM ký).
            var opinions = await db.PurchaseEvaluationLevelOpinions
                .Where(o => o.PurchaseEvaluationId == pe.Id).ToListAsync();
            var ccmLevelId = wf.Steps.First(s => s.Order == 1).Levels.First(l => l.Order == 1).Id;
            opinions.Should().ContainSingle(o => o.ApprovalWorkflowLevelId == ccmLevelId,
                "chỉ slot CCM ký, không ghi hộ CEO");
        }
    }

    // =====================================================================
    // 1a. (NEW Mig 54) flag=false + gói < ngưỡng → KHÔNG finalize, advance lên CEO.
    //     Đây là ĐỔI hành vi chính: trước S69 AUTO finalize, NAY phải tích flag.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_NoDelegationFlag_CcmBelowThreshold_DoesNotFinalize_AdvancesToCeo()
    {
        var (svc, fix, db, clock) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccm1a@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceo1a@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },   // Bước 1 = CCM
                new[] { ceo },   // Bước 2 = CEO
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-001A");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Gói 500tr < ngưỡng — NHƯNG KHÔNG tích uỷ-quyền.
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: ValidApprovedPrice);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            // flag=false (mặc định) → KHÔNG vào finalize-branch dù gói < ngưỡng.
            await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl });

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "không tích uỷ-quyền → KHÔNG finalize dù gói < ngưỡng → advance bình thường lên CEO");
            pe.CurrentWorkflowStepIndex.Should().Be(1, "advance sang Bước 2 (CEO)");
            pe.CurrentApprovalLevelOrder.Should().Be(1, "Cấp 1 của Bước 2");
            pe.SlaDeadline.Should().Be(clock.UtcNow.AddDays(7), "SLA reset cho CEO");
            // Chưa finalize → giá chốt CHƯA bind (CEO mới chọn ở cấp cuối).
            pe.ApprovedPriceAmount.Should().BeNull("chưa tới terminal → chưa có giá chốt");
        }
    }

    // =====================================================================
    // 2. flag=false + gói >= ngưỡng → advance sang CEO, Phase GIỮ ChoDuyet.
    //    Boundary: gói == đúng ngưỡng (strict-less-than → KHÔNG finalize kể cả nếu
    //    có tích flag — case b cover phần Conflict).
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmAtThreshold_NoFlag_AdvancesToCeo_PhaseStaysChoDuyet()
    {
        var (svc, fix, db, clock) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccm2@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceo2@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },   // Bước 1 = CCM
                new[] { ceo },   // Bước 2 = CEO
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-002");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Gói == ĐÚNG ngưỡng (1 tỷ) → strict `<` false → KHÔNG finalize.
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: CeoThreshold);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl });

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "gói == ngưỡng (không < ngưỡng) → advance bình thường, chưa DaDuyet");
            pe.CurrentWorkflowStepIndex.Should().Be(1, "advance sang Bước 2 (CEO)");
            pe.CurrentApprovalLevelOrder.Should().Be(1, "Cấp 1 của Bước 2");
            pe.SlaDeadline.Should().Be(clock.UtcNow.AddDays(7), "SLA reset cho approver kế (CEO)");
        }
    }

    // =====================================================================
    // (b) flag=true + gói == ngưỡng → ConflictException (strict `<` violated:
    //     winnerQuoteTotal >= ceoThreshold). Phiếu KHÔNG đổi (throw trước mutate).
    // =====================================================================
    [Fact]
    public async Task ApproveV2_DelegationFlag_AtThreshold_ThrowsConflict_NoMutation()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccmB@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceoB@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },
                new[] { ceo },
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-00B");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Gói == ngưỡng → finalize-branch chạy nhưng `>= ceoThreshold` → Conflict.
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: CeoThreshold);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*ngưỡng CEO*");

            // Side-effect: throw TRƯỚC khi set Phase → vẫn ChoDuyet, pointer giữ nguyên.
            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "gói >= ngưỡng → không được finalize");
            reloaded.CurrentWorkflowStepIndex.Should().Be(0);
            reloaded.ApprovedPriceAmount.Should().BeNull();
        }
    }

    // =====================================================================
    // (b2) flag=true + gói > ngưỡng (vượt rõ rệt) → cũng ConflictException.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_DelegationFlag_AboveThreshold_ThrowsConflict()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccmB2@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceoB2@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },
                new[] { ceo },
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-0B2");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: 2_000_000_000m);  // > ngưỡng
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().ThrowAsync<ConflictException>();

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "gói > ngưỡng → CEO vẫn phải duyệt");
        }
    }

    // =====================================================================
    // (c) flag=true + actor KHÔNG phải CostControl → ForbiddenException.
    //     Dùng PRO (Procurement) đứng tại slot bước 1 + tích uỷ-quyền + gói < ngưỡng.
    //     Guard role (line 835) chạy TRƯỚC threshold-compare → Forbidden.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_DelegationFlag_NonCostControlActor_ThrowsForbidden()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            // Bước 1 Cấp 1 = Procurement (KHÔNG phải CCM) — đứng tại đây tích flag.
            var pro = (await fix.CreateUserAsync("proC@fb.test", "PRO User", null, new[] { AppRoles.Procurement })).Id;
            var ceo = (await fix.CreateUserAsync("ceoC@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { pro },
                new[] { ceo },
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-00C");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: ValidApprovedPrice);  // < ngưỡng
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, pro, new[] { AppRoles.Procurement },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().ThrowAsync<ForbiddenException>()
                .WithMessage("*CCM*");

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "PRO không được uỷ-quyền finalize");
        }
    }

    // =====================================================================
    // (d) flag=true + workflow chưa set CeoApprovalThreshold (null) → ConflictException.
    //     Threshold-null check (line 832) chạy TRƯỚC role/value → Conflict.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_DelegationFlag_ThresholdNotSet_ThrowsConflict()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccmD@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceoD@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },
                new[] { ceo },
            }, ceoThreshold: null);   // ⭐ ngưỡng null → finalize bị chặn ngay

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-00D");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: ValidApprovedPrice);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*Ngưỡng giá trị*");

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "ngưỡng null → không thể uỷ-quyền finalize");
        }
    }

    // =====================================================================
    // 3. CeoApprovalThreshold == null + flag=false → advance tuyến tính KỂ CẢ CCM.
    //    Backward-compat: workflow chưa set ngưỡng → behavior cũ (no early-finalize).
    // =====================================================================
    [Fact]
    public async Task ApproveV2_ThresholdNull_NoFlag_CcmApprovesBelowAnyValue_AdvancesNormally()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccm3@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceo3@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },
                new[] { ceo },
            }, ceoThreshold: null);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-003");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Gói 1đ (siêu nhỏ) nhưng ngưỡng null + KHÔNG flag.
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: 1m);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl });

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "ngưỡng null + no flag → advance bình thường");
            pe.CurrentWorkflowStepIndex.Should().Be(1, "advance sang Bước 2 (CEO) như cũ");
            pe.CurrentApprovalLevelOrder.Should().Be(1);
        }
    }

    // =====================================================================
    // 4. Non-CCM actor (Procurement) duyệt dưới ngưỡng, KHÔNG flag → advance bình
    //    thường (no early-finalize). Cover nhận-diện-theo-role + no-flag.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_NonCcmActor_NoFlag_BelowThreshold_AdvancesNormally()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var pro = (await fix.CreateUserAsync("pro4@fb.test", "PRO User", null, new[] { AppRoles.Procurement })).Id;
            var ceo = (await fix.CreateUserAsync("ceo4@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { pro },
                new[] { ceo },
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-004");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: 100_000_000m);  // << ngưỡng
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, pro, new[] { AppRoles.Procurement });

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "PRO duyệt + gói nhỏ + no flag → advance bình thường");
            pe.CurrentWorkflowStepIndex.Should().Be(1, "advance sang Bước 2 (CEO) bình thường");
            pe.CurrentApprovalLevelOrder.Should().Be(1);
        }
    }

    // =====================================================================
    // 5. CCM tích uỷ-quyền (flag=true) + ở slot CUỐI (Bước cuối Cấp cuối) + dưới
    //    ngưỡng + truyền giá → DaDuyet qua finalize-branch (chạy TRƯỚC normal advance).
    //    Verify: chỉ 1 Approval Approve row (không double-finalize).
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmDelegationFlag_AtLastSlot_BelowThreshold_FinalizesDaDuyet_NoDoubleApproval()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            // Workflow 1 bước, 1 cấp = CCM (đây là slot CUỐI luôn).
            var ccm = (await fix.CreateUserAsync("ccm5@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },   // Bước 1 Cấp 1 = CCM = slot cuối
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-005");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: 300_000_000m);  // < ngưỡng
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: 300_000_000m, approvedPriceSource: ValidApprovedSource);

            // finalize-branch (line 830) chạy TRƯỚC normal advance → DaDuyet ngay.
            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet,
                "CCM tích uỷ-quyền ở slot cuối + dưới ngưỡng → DaDuyet qua finalize-branch");
            pe.CurrentWorkflowStepIndex.Should().BeNull();
            pe.CurrentApprovalLevelOrder.Should().BeNull();
            pe.ApprovedPriceAmount.Should().Be(300_000_000m);

            // KHÔNG double: chỉ 1 Approval Approve (của CCM), finalize-branch return ngay.
            var approvals = await db.PurchaseEvaluationApprovals
                .Where(a => a.PurchaseEvaluationId == pe.Id
                    && a.Decision == ApprovalDecision.Approve).ToListAsync();
            approvals.Should().HaveCount(1, "1 lần CCM duyệt = 1 Approval row, không double");
        }
    }

    // =====================================================================
    // 6. CCM tích uỷ-quyền + đủ điều kiện NHƯNG KHÔNG truyền giá chốt (null) →
    //    ConflictException ("Chọn 1 giá chốt..."). Finalize cũng cần giá chốt như
    //    terminal thường (ApplyApprovedPriceOnFinalize cho human approver).
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmDelegationFlag_BelowThreshold_NullPrice_ThrowsConflict()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccm6@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceo6@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },
                new[] { ceo },
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-006");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: ValidApprovedPrice);  // < ngưỡng
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            // flag=true + đủ ĐK threshold/role/value NHƯNG approvedPriceAmount=null.
            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: null, approvedPriceSource: null);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*giá chốt*");
        }
    }

    // =====================================================================
    // (c) [multi-NCC 2026-07-12 cardinality-change — NEW] THRESHOLD-CROSSING:
    //     Winner có 1 quote ĐƯỢC CHỌN 500tr + 1 quote KHÔNG chọn 700tr.
    //     • Tổng-theo-CŨ (whole-supplier IsWinner, gồm cả quote không chọn) = 1.2 tỷ ≥ ngưỡng
    //       1 tỷ → nếu code còn dùng logic cũ = Conflict (SAI).
    //     • Tổng-theo-MỚI (Quote.IsSelected only) = 500tr < 1 tỷ → CCM duyệt done miễn CEO
    //       HỢP LỆ → finalize DaDuyet, KHÔNG throw.
    //     Chống regression: chứng minh winnerQuoteTotal đọc IsSelected-subset, KHÔNG whole-supplier.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmDelegation_WholeSupplierSumAboveButSelectedSubsetBelow_Finalizes_NoThrow()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccmXc@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceoXc@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },   // Bước 1 = CCM (đứng đây)
                new[] { ceo },   // Bước 2 = CEO (sẽ bị bỏ qua)
            }, ceoThreshold: CeoThreshold);   // 1 tỷ

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-XC");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // Selected 500tr + Unselected 700tr → whole-supplier=1.2 tỷ ≥ ngưỡng, IsSelected=500tr < ngưỡng.
            var supplierId = await SeedWinnerWithSelectedAndUnselectedQuoteAsync(
                db, pe, selectedThanhTien: 500_000_000m, unselectedThanhTien: 700_000_000m);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            // finalize theo IsSelected-subset (500tr < 1 tỷ) → KHÔNG throw, DaDuyet.
            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().NotThrowAsync(
                "winnerQuoteTotal đọc SUM Quote.IsSelected (500tr < ngưỡng); logic cũ whole-supplier 1.2 tỷ đã bị thay");
            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "CCM duyệt done miễn CEO — gói ĐƯỢC CHỌN < ngưỡng");
            pe.CurrentWorkflowStepIndex.Should().BeNull();
            pe.ApprovedPriceAmount.Should().Be(ValidApprovedPrice);
        }
    }

    // [S116 anh Kiệt — SỐ ÂM báo giá] Seed 1 winner mang 2 báo giá ĐƯỢC CHỌN trái dấu
    // (mỗi giá 1 detail) → winnerQuoteTotal = SUM có dấu (NET). Cho test threshold theo
    // NET: R-CEO giữ ngưỡng NET, dòng âm (phát-sinh-giảm / hoàn tiền) kéo tổng xuống.
    private static async Task<Guid> SeedWinnerWithTwoSelectedSignedQuotesAsync(
        TestApplicationDbContext db, PurchaseEvaluation pe, decimal a, decimal b)
    {
        var supplierId = Guid.NewGuid();
        var pes = new PurchaseEvaluationSupplier
        {
            PurchaseEvaluationId = pe.Id, SupplierId = supplierId, Order = 0, IsWinner = true,
        };
        var d1 = new PurchaseEvaluationDetail
        {
            PurchaseEvaluationId = pe.Id, GroupCode = "A.I", GroupName = "Vật tư", NoiDung = "Dòng +", Order = 0,
        };
        var d2 = new PurchaseEvaluationDetail
        {
            PurchaseEvaluationId = pe.Id, GroupCode = "A.II", GroupName = "Phát sinh", NoiDung = "Dòng −", Order = 1,
        };
        db.PurchaseEvaluationSuppliers.Add(pes);
        db.PurchaseEvaluationDetails.Add(d1);
        db.PurchaseEvaluationDetails.Add(d2);
        db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
        {
            PurchaseEvaluationDetailId = d1.Id, PurchaseEvaluationSupplierId = pes.Id, ThanhTien = a, IsSelected = true,
        });
        db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
        {
            PurchaseEvaluationDetailId = d2.Id, PurchaseEvaluationSupplierId = pes.Id, ThanhTien = b, IsSelected = true,
        });
        await db.SaveChangesAsync(CancellationToken.None);
        return supplierId;
    }

    // =====================================================================
    // (d) [S116 anh Kiệt — SỐ ÂM báo giá] NET threshold: dòng âm (phát-sinh-giảm /
    //     hoàn tiền) kéo winnerQuoteTotal (SUM có dấu) xuống DƯỚI ngưỡng CEO → CCM
    //     duyệt done miễn CEO HỢP LỆ. R-CEO giữ ngưỡng NET (KHÔNG đổi) — chỉ tổng đại
    //     số quyết định, KHÔNG phải dòng dương lớn nhất.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmDelegation_NegativeLinePullsNetBelowThreshold_Finalizes_NoThrow()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccmNeg@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceoNeg@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },   // Bước 1 = CCM (đứng đây)
                new[] { ceo },   // Bước 2 = CEO (sẽ bị bỏ qua)
            }, ceoThreshold: CeoThreshold);   // 1 tỷ

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-NEG");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // +1.2 tỷ (vật tư) + (−0.3 tỷ) (giảm trừ) → NET 0.9 tỷ < ngưỡng 1 tỷ.
            // Dòng dương RIÊNG (1.2 tỷ) ≥ ngưỡng — nếu code lấy max-line / positive-only = SAI.
            var supplierId = await SeedWinnerWithTwoSelectedSignedQuotesAsync(
                db, pe, a: 1_200_000_000m, b: -300_000_000m);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().NotThrowAsync(
                "winnerQuoteTotal = SUM có dấu (1.2 tỷ − 0.3 tỷ = 0.9 tỷ) < ngưỡng → CCM finalize hợp lệ");
            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "net dưới ngưỡng → duyệt done miễn CEO");
            pe.CurrentWorkflowStepIndex.Should().BeNull();
            pe.ApprovedPriceAmount.Should().Be(ValidApprovedPrice);
        }
    }

    // =====================================================================
    // (e) [S116] CONTRAST — CÙNG dòng dương 1.2 tỷ NHƯNG KHÔNG có dòng âm → NET 1.2 tỷ
    //     ≥ ngưỡng → Conflict. Chứng minh dòng âm ở (d) là LOAD-BEARING (chính nó kéo
    //     net xuống mới cho finalize), test (d) KHÔNG phải happy-path trivial.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_CcmDelegation_SamePositiveLineWithoutNegative_NetAboveThreshold_ThrowsConflict()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ccm = (await fix.CreateUserAsync("ccmNeg2@fb.test", "CCM User", null, new[] { AppRoles.CostControl })).Id;
            var ceo = (await fix.CreateUserAsync("ceoNeg2@fb.test", "CEO User", null, new[] { AppRoles.Director })).Id;

            var wf = await SeedWorkflowAsync(db, new[]
            {
                new[] { ccm },
                new[] { ceo },
            }, ceoThreshold: CeoThreshold);

            var pe = BuildPeAtApprovalSlot(wf.Id, selectedSupplierId: null, stepIdx: 0, levelOrder: 1, code: "PE-FB-NEG2");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);
            // CHỈ dòng +1.2 tỷ (KHÔNG dòng âm) → net 1.2 tỷ ≥ ngưỡng.
            var supplierId = await SeedWinnerWithQuoteAsync(db, pe, quoteThanhTien: 1_200_000_000m);
            pe.SelectedSupplierId = supplierId;
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ccm, new[] { AppRoles.CostControl },
                finalizeByCcmDelegation: true,
                approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*ngưỡng CEO*");

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "không có dòng âm → net ≥ ngưỡng → CEO vẫn phải duyệt");
        }
    }
}
