using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Notifications;
using SolutionErp.Application.PurchaseEvaluations.Services;  // WorkflowReturnMode enum
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts;     // ApprovalDecision shared
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Notifications;
using SolutionErp.Domain.PurchaseEvaluations;
using SolutionErp.Infrastructure.Services;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Services;

// Plan C task 1-2 catch-up cho S21 t4-t5 feature + Plan K Chunk F (S23 t1 Mig 31):
// - ApplyReturnModeAsync 4 mode đọc level.Allow* per-NV (Mig 29 refactor từ workflow-level Mig 28)
// - skipToFinal (Mig 31 refactor) đọc matchingLevel.AllowApproverSkipToFinal per-Approver-slot
//   trong ApproveV2Async branch APPROVE STEP. Semantic mới: Approver during ChoDuyet
//   skip thẳng Cấp cuối → Phase=DaDuyet terminal. Semantic cũ Drafter-from-Nháp
//   (Mig 29) đã deprecated + storage trên Users table đã drop trong Mig 31 Plan K.
//
// Focus: defensive boundary check + admin bypass invariant. KHÔNG cover toàn bộ
// edge case (Bước 1 Cấp 1 fallback, Assignee runtime pick, V1 legacy fallback) —
// defer khi UAT lộ regression cụ thể.
public class PurchaseEvaluationWorkflowServiceReturnModeTests
{
    private static (PurchaseEvaluationWorkflowService svc, IdentityFixture fix, TestApplicationDbContext db, FixedDateTime dt)
        CreateService()
    {
        var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var um = fix.Services.GetRequiredService<UserManager<User>>();
        var dt = new FixedDateTime(new DateTime(2026, 5, 13, 0, 0, 0, DateTimeKind.Utc));
        var notify = new NoOpNotificationService();
        var svc = new PurchaseEvaluationWorkflowService(db, dt, notify, um);
        return (svc, fix, db, dt);
    }

    // Workflow setup: 1 Bước (1 Step) — 2 Cấp (2 Levels) — mỗi Cấp 1 Approver.
    // Mặc định mọi Allow* = false trên Level slot (admin opt-in pattern Mig 29).
    // ApproverUserId mặc định = approverId truyền vào (caller có thể override).
    // Plan M S23 t3 — thêm 2 param L1 (`allowReturnOneLevelL1` + `allowReturnOneStepL2`)
    // phục vụ test edge case Bước 1 Cấp 1 reset ChoDuyet (không lùi được).
    private static async Task<(ApprovalWorkflow wf, ApprovalWorkflowStep step, ApprovalWorkflowLevel l1, ApprovalWorkflowLevel l2)>
        SeedWorkflowAsync(
            TestApplicationDbContext db,
            Guid approver1UserId,
            Guid approver2UserId,
            bool allowReturnOneLevelL2 = false,
            bool allowReturnToDrafterL2 = false,
            bool allowApproverEditL2 = false,
            bool allowReturnOneLevelL1 = false,
            bool allowReturnOneStepL2 = false)
    {
        var wf = new ApprovalWorkflow
        {
            Id = Guid.NewGuid(),
            Code = "QT-TEST-001",
            Version = 1,
            Name = "Test Workflow per-NV",
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            IsActive = true,
            IsUserSelectable = true,
        };
        var step = new ApprovalWorkflowStep
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowId = wf.Id,
            Order = 1,
            DepartmentId = null,   // Step.DepartmentId là hint nullable — skip FK seeding Department trong test
            Name = "Bước 1 CCM",
        };
        var l1 = new ApprovalWorkflowLevel
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowStepId = step.Id,
            Order = 1,
            ApproverUserId = approver1UserId,
            AllowReturnOneLevel = allowReturnOneLevelL1,
            // Các Allow* khác mặc định false (sad path)
        };
        var l2 = new ApprovalWorkflowLevel
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowStepId = step.Id,
            Order = 2,
            ApproverUserId = approver2UserId,
            AllowReturnOneLevel = allowReturnOneLevelL2,
            AllowReturnOneStep = allowReturnOneStepL2,
            AllowReturnToDrafter = allowReturnToDrafterL2,
            AllowApproverEditDetails = allowApproverEditL2,
        };
        db.ApprovalWorkflows.Add(wf);
        db.ApprovalWorkflowSteps.Add(step);
        db.ApprovalWorkflowLevels.Add(l1);
        db.ApprovalWorkflowLevels.Add(l2);
        await db.SaveChangesAsync(CancellationToken.None);
        return (wf, step, l1, l2);
    }

    private static PurchaseEvaluation BuildPeAtLevel2(Guid workflowId, Guid drafterId, string code = "PE-RM-001")
    {
        return new PurchaseEvaluation
        {
            Id = Guid.NewGuid(),
            Type = PurchaseEvaluationType.DuyetNcc,
            Phase = PurchaseEvaluationPhase.ChoDuyet,
            MaPhieu = code,
            TenGoiThau = "Test return mode",
            ProjectId = Guid.NewGuid(),
            DrafterUserId = drafterId,
            ApprovalWorkflowId = workflowId,
            CurrentWorkflowStepIndex = 0,   // Bước 1 (Step Order=1)
            CurrentApprovalLevelOrder = 2,  // Cấp 2 đang chờ duyệt
        };
    }

    // ============ Task 1: ApplyReturnModeAsync ============

    private static async Task<(User a1, User a2)> SeedApproversAsync(IdentityFixture fix, string suffix)
    {
        var a1 = await fix.CreateUserAsync($"approver1-{suffix}@test.local", $"Approver1 {suffix}", departmentId: null, roles: new[] { AppRoles.CostControl });
        var a2 = await fix.CreateUserAsync($"approver2-{suffix}@test.local", $"Approver2 {suffix}", departmentId: null, roles: new[] { AppRoles.CostControl });
        return (a1, a2);
    }

    [Fact]
    public async Task ReturnMode_Drafter_AllowedByLevel_SetsPhaseTraLaiAndClearsPointer()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "rm1");
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnToDrafterL2: true);
            var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid());
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: a2.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Reject,
                comment: "trả về drafter sửa",
                returnMode: WorkflowReturnMode.Drafter,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.TraLai);
            pe.CurrentWorkflowStepIndex.Should().BeNull("Drafter mode clear step pointer");
            pe.CurrentApprovalLevelOrder.Should().BeNull("Drafter mode clear level pointer");
            pe.SlaDeadline.Should().BeNull();
        }
    }

    [Fact]
    public async Task ReturnMode_Drafter_DeniedByLevel_NonAdmin_Throws()
    {
        // Level Allow*=false (default) + non-admin → throw ConflictException.
        // Pattern: admin Designer KHÔNG tick flag cho Level slot này → Approver
        // không được trả về Drafter mode (phải dùng mode khác hoặc Trả lại admin).
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "rm2");
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnToDrafterL2: false);
            var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid(), code: "PE-RM-002");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: a2.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Reject,
                comment: "test denied",
                returnMode: WorkflowReturnMode.Drafter,
                ct: CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*Cấp Approver hiện tại không bật mode*Drafter*");
            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "Guard chặn trước mutate");
        }
    }

    [Fact]
    public async Task ReturnMode_OneLevel_AllowedByLevel_LowersLevelPointer()
    {
        // Level 2 → Level 1 trong cùng Step (peer review chain).
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "rm3");
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnOneLevelL2: true);
            var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid(), code: "PE-RM-003");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: a2.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Reject,
                comment: "trả về 1 Cấp",
                returnMode: WorkflowReturnMode.OneLevel,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "OneLevel giữ ChoDuyet (peer review chain), KHÔNG về TraLai");
            pe.CurrentApprovalLevelOrder.Should().Be(1, "Lùi 1 Cấp (từ 2 → 1)");
            pe.CurrentWorkflowStepIndex.Should().Be(0, "Giữ Step hiện tại");
        }
    }

    [Fact]
    public async Task ReturnMode_OneLevel_DeniedByLevel_AdminBypass_Succeeds()
    {
        // Admin override workflow.Allow* check — vẫn được trả lại dù slot disabled.
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "rm4");
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnOneLevelL2: false);
            var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid(), code: "PE-RM-004");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: a2.Id,
                actorRoles: new[] { AppRoles.Admin },
                decision: ApprovalDecision.Reject,
                comment: "admin override",
                returnMode: WorkflowReturnMode.OneLevel,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet);
            pe.CurrentApprovalLevelOrder.Should().Be(1,
                "Admin bypass Allow* flag — vẫn lùi pointer");
        }
    }

    // ============ Plan M S23 t3 — F1 edge case Bước 1 reset ChoDuyet ============
    // Cũ: Approver Bước 1 Cấp 1 click "Trả lại 1 Cấp" / "Trả lại 1 Bước" → fallback
    // Drafter mode (Phase=TraLai, clear pointer). UX confusing: Approver A đột nhiên
    // bị "đẩy phiếu về Drafter" mặc dù mình chính là người đang giữ phiếu.
    // Mới: reset (Step=0, Level=1) GIỮ Phase=ChoDuyet — no-op effective (Approver A
    // vẫn giữ phiếu), SLA reset 7d, audit log ContextNote "không lùi được". 2 test
    // dưới cover OneLevel + OneStep edge case riêng — Drafter mode (line 268-275)
    // GIỮ semantic Phase=TraLai unchanged.

    [Fact]
    public async Task ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet()
    {
        // Workflow 1 Step × 2 Levels, PE đang Bước 1 Cấp 1 (Approver A duyệt).
        // Slot Cấp 1 tick AllowReturnOneLevel=true (admin opt-in pattern Mig 29).
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "rm-edge-onelevel");
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnOneLevelL1: true);

            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-RM-EDGE-OL",
                TenGoiThau = "OneLevel edge Bước 1 Cấp 1",
                ProjectId = Guid.NewGuid(),
                DrafterUserId = Guid.NewGuid(),
                ApprovalWorkflowId = wf.Id,
                CurrentWorkflowStepIndex = 0,   // Bước 1
                CurrentApprovalLevelOrder = 1,  // Cấp 1 — edge case "không lùi được"
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: a1.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Reject,
                comment: "thử trả 1 cấp tại Bước 1 Cấp 1",
                returnMode: WorkflowReturnMode.OneLevel,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "Plan M edge case: GIỮ ChoDuyet (no-op effective), KHÔNG fallback Drafter TraLai");
            pe.CurrentWorkflowStepIndex.Should().Be(0, "Pointer reset Bước 1");
            pe.CurrentApprovalLevelOrder.Should().Be(1, "Pointer reset Cấp 1");
            pe.SlaDeadline.Should().NotBeNull("SLA reset 7d cho Approver A giữ phiếu");

            // Audit log "không lùi được" phải xuất hiện ở ContextNote (LogTransitionAsync
            // append summary từ ApplyReturnModeAsync vào field này — line 96-99 service).
            // Summary field cố định "Chuyển phase {from} → {to}".
            // Plan AB S25 thêm Changelog entry mới (EntityType=Workflow + Summary="Trả lại (...)"
            // + ContextNote=null) → cùng CreatedAt SQLite tie-break non-deterministic. Filter
            // by Summary "Chuyển phase" để pick đúng LogTransitionAsync entry.
            var changelog = await db.PurchaseEvaluationChangelogs
                .Where(c => c.PurchaseEvaluationId == pe.Id && c.Summary!.Contains("Chuyển phase"))
                .OrderByDescending(c => c.CreatedAt)
                .FirstAsync();
            changelog.ContextNote.Should().NotBeNull();
            changelog.ContextNote!.Should().Contain("không lùi được",
                "Audit trail rõ ràng cho UAT review — phân biệt no-op vs lùi thật");
        }
    }

    [Fact]
    public async Task ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet()
    {
        // Workflow 1 Step × 2 Levels, PE đang Bước 1 Cấp 2 (Approver B duyệt).
        // Slot Cấp 2 tick AllowReturnOneStep=true. Action OneStep từ Bước 1 → edge
        // case "không lùi được Bước" (vẫn là first Step) → reset (0, 1) giữ ChoDuyet.
        // Approver B sau reset bị bàn giao về Approver A (Cấp 1) — vẫn trong chuỗi duyệt.
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "rm-edge-onestep");
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnOneStepL2: true);
            var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid(), code: "PE-RM-EDGE-OS");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: a2.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Reject,
                comment: "thử trả 1 bước tại Bước 1",
                returnMode: WorkflowReturnMode.OneStep,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
                "Plan M edge case: GIỮ ChoDuyet (no-op effective), KHÔNG fallback Drafter TraLai");
            pe.CurrentWorkflowStepIndex.Should().Be(0, "Pointer reset Bước 1");
            pe.CurrentApprovalLevelOrder.Should().Be(1, "Pointer reset Cấp 1 (bàn giao về Approver A)");
            pe.SlaDeadline.Should().NotBeNull("SLA reset 7d cho approver mới");

            // Plan AB S25 — thêm Changelog Workflow entry, filter LogTransition entry
            // qua Summary "Chuyển phase" để pick đúng row có ContextNote.
            var changelog = await db.PurchaseEvaluationChangelogs
                .Where(c => c.PurchaseEvaluationId == pe.Id && c.Summary!.Contains("Chuyển phase"))
                .OrderByDescending(c => c.CreatedAt)
                .FirstAsync();
            changelog.ContextNote.Should().NotBeNull();
            changelog.ContextNote!.Should().Contain("không lùi được",
                "Audit trail rõ ràng cho UAT review — phân biệt no-op vs lùi thật");
        }
    }

    // ============ Task 2: skipToFinal — Mig 31 (S23 t1 Plan K Chunk F) ============
    // Semantic mới: Approver during ChoDuyet duyệt thẳng Cấp cuối → Phase=DaDuyet terminal.
    // Storage: matchingLevel.AllowApproverSkipToFinal (per-Approver-slot, admin opt-in).
    // 3 tests cover happy path + denied + admin bypass — mirror F1 Drafter pattern.
    //
    // SeedApproverF2WorkflowAsync helper: 2 Steps × 2 Levels (multi-step để verify
    // skip thẳng terminal, KHÔNG fallthrough advance pointer next Step). Slot Cấp 1
    // Bước 1 set AllowApproverSkipToFinal per param.

    private static async Task<(ApprovalWorkflow wf, ApprovalWorkflowStep s1, ApprovalWorkflowLevel s1l1, ApprovalWorkflowLevel s1l2, ApprovalWorkflowStep s2, ApprovalWorkflowLevel s2l1, ApprovalWorkflowLevel s2l2)>
        SeedApproverF2WorkflowAsync(
            TestApplicationDbContext db,
            Guid s1l1Approver,
            Guid s1l2Approver,
            Guid s2l1Approver,
            Guid s2l2Approver,
            bool allowApproverSkipToFinalSlotS1L1 = false)
    {
        var wf = new ApprovalWorkflow
        {
            Id = Guid.NewGuid(),
            Code = "QT-TEST-F2-001",
            Version = 1,
            Name = "Test Workflow Approver F2",
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            IsActive = true,
            IsUserSelectable = true,
        };
        var s1 = new ApprovalWorkflowStep
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowId = wf.Id,
            Order = 1,
            DepartmentId = null,
            Name = "Bước 1 CCM",
        };
        var s2 = new ApprovalWorkflowStep
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowId = wf.Id,
            Order = 2,
            DepartmentId = null,
            Name = "Bước 2 GĐ",
        };
        var s1l1 = new ApprovalWorkflowLevel
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowStepId = s1.Id,
            Order = 1,
            ApproverUserId = s1l1Approver,
            AllowApproverSkipToFinal = allowApproverSkipToFinalSlotS1L1,
        };
        var s1l2 = new ApprovalWorkflowLevel
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowStepId = s1.Id,
            Order = 2,
            ApproverUserId = s1l2Approver,
        };
        var s2l1 = new ApprovalWorkflowLevel
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowStepId = s2.Id,
            Order = 1,
            ApproverUserId = s2l1Approver,
        };
        var s2l2 = new ApprovalWorkflowLevel
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowStepId = s2.Id,
            Order = 2,
            ApproverUserId = s2l2Approver,
        };
        db.ApprovalWorkflows.Add(wf);
        db.ApprovalWorkflowSteps.Add(s1);
        db.ApprovalWorkflowSteps.Add(s2);
        db.ApprovalWorkflowLevels.Add(s1l1);
        db.ApprovalWorkflowLevels.Add(s1l2);
        db.ApprovalWorkflowLevels.Add(s2l1);
        db.ApprovalWorkflowLevels.Add(s2l2);
        await db.SaveChangesAsync(CancellationToken.None);
        return (wf, s1, s1l1, s1l2, s2, s2l1, s2l2);
    }

    [Fact]
    public async Task ApproveV2_SkipToFinal_AdminTickFlag_AdvancesToLastSlot()
    {
        // Happy path (L1 S23 t2 semantic refactor): workflow 2 Step × 2 Level.
        // Slot Cấp 1 Bước 1 admin tick AllowApproverSkipToFinal=true. Actor = userA
        // (Cấp 1 Bước 1 approver, non-admin role). PE pin workflow + Phase=ChoDuyet
        // + pointer init Step 0 Cấp 1.
        // → Phase GIỮ ChoDuyet, pointer advance tới last Step (idx=1) + last Level
        // (Order=2), opinion + PEA + Changelog logged. NV cuối vẫn cần ký thật để
        // tiến DaDuyet (KHÔNG auto-approve terminal — S23 t2 fix bro UAT).
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var userA = await fix.CreateUserAsync("usera-f2-skip@test.local", "User A F2 Skip",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userB = await fix.CreateUserAsync("userb-f2-skip@test.local", "User B F2 Skip",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userC = await fix.CreateUserAsync("userc-f2-skip@test.local", "User C F2 Skip",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userD = await fix.CreateUserAsync("userd-f2-skip@test.local", "User D F2 Skip",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var (wf, _, s1l1, _, _, _, _) = await SeedApproverF2WorkflowAsync(
                db, userA.Id, userB.Id, userC.Id, userD.Id,
                allowApproverSkipToFinalSlotS1L1: true);

            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-F2-001",
                TenGoiThau = "Approver F2 happy",
                ProjectId = Guid.NewGuid(),
                DrafterUserId = Guid.NewGuid(),
                ApprovalWorkflowId = wf.Id,
                CurrentWorkflowStepIndex = 0,
                CurrentApprovalLevelOrder = 1,
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.ChoDuyet,
                actorUserId: userA.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Approve,
                comment: "duyệt thẳng cấp cuối",
                skipToFinal: true,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "L1 S23 t2: skip advance pointer, NV cuối duyệt thật để DaDuyet");
            pe.CurrentWorkflowStepIndex.Should().Be(1, "Pointer advance tới lastStepIdx (Bước cuối)");
            pe.CurrentApprovalLevelOrder.Should().Be(2, "Pointer advance tới lastLevelMaxOrder (Cấp cuối Bước cuối)");
            pe.SlaDeadline.Should().NotBeNull("SLA reset 7d cho NV cuối nhận lại");

            // 1 PEL opinion UPSERT cho slot Cấp 1 Bước 1 trước skip
            var opinions = await db.PurchaseEvaluationLevelOpinions
                .Where(o => o.PurchaseEvaluationId == pe.Id).ToListAsync();
            opinions.Should().HaveCount(1);
            opinions[0].ApprovalWorkflowLevelId.Should().Be(s1l1.Id, "Opinion ghi vào slot Cấp 1 Bước 1");
            opinions[0].SignedByUserId.Should().Be(userA.Id);

            // 1 PEA approval audit
            var approvals = await db.PurchaseEvaluationApprovals
                .Where(a => a.PurchaseEvaluationId == pe.Id).ToListAsync();
            approvals.Should().HaveCount(1);
            approvals[0].ApproverUserId.Should().Be(userA.Id);
            approvals[0].Decision.Should().Be(ApprovalDecision.Approve);

            // Changelog entry với context note chứa "Approver skip thẳng" + "Bước/Cấp"
            var changelogs = await db.PurchaseEvaluationChangelogs
                .Where(c => c.PurchaseEvaluationId == pe.Id).ToListAsync();
            changelogs.Should().Contain(c => c.ContextNote != null
                && c.ContextNote.Contains("Approver skip thẳng"));
        }
    }

    [Fact]
    public async Task ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException()
    {
        // Denied: workflow same nhưng AllowApproverSkipToFinal=false cho slot Cấp 1 Bước 1.
        // Actor = userA (non-admin, trong slot Cấp 1 Bước 1 approvers).
        // → throw ConflictException "chưa được phép duyệt thẳng Cấp cuối".
        // State unchanged: Phase still ChoDuyet, pointer unchanged.
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var userA = await fix.CreateUserAsync("usera-f2-deny@test.local", "User A F2 Deny",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userB = await fix.CreateUserAsync("userb-f2-deny@test.local", "User B F2 Deny",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userC = await fix.CreateUserAsync("userc-f2-deny@test.local", "User C F2 Deny",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userD = await fix.CreateUserAsync("userd-f2-deny@test.local", "User D F2 Deny",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var (wf, _, _, _, _, _, _) = await SeedApproverF2WorkflowAsync(
                db, userA.Id, userB.Id, userC.Id, userD.Id,
                allowApproverSkipToFinalSlotS1L1: false);

            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-F2-002",
                TenGoiThau = "Approver F2 denied",
                ProjectId = Guid.NewGuid(),
                DrafterUserId = Guid.NewGuid(),
                ApprovalWorkflowId = wf.Id,
                CurrentWorkflowStepIndex = 0,
                CurrentApprovalLevelOrder = 1,
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.ChoDuyet,
                actorUserId: userA.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Approve,
                comment: "test denied",
                skipToFinal: true,
                ct: CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*chưa được phép duyệt thẳng Cấp cuối*");

            // State: in-memory PE chưa mutate phase (throw chặn trước assign).
            // Note: service log PEA + Opinion TRƯỚC khi validate skipToFinal flag,
            // throw chặn SaveChangesAsync → DB không persist. Focus test contract
            // throw + Phase invariant unchanged.
            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "Throw chặn trước mutate Phase");
            pe.CurrentWorkflowStepIndex.Should().Be(0, "Pointer unchanged");
            pe.CurrentApprovalLevelOrder.Should().Be(1, "Pointer unchanged");
        }
    }

    [Fact]
    public async Task ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck()
    {
        // Admin bypass (L1 S23 t2 semantic): workflow same nhưng AllowApproverSkipToFinal=false
        // cho slot Cấp 1 Bước 1. Actor = adminUser (actorRoles contains "Admin"), trong slot.
        // → Phase GIỮ ChoDuyet, pointer advance tới last slot (idx=1, Order=2),
        // opinion logged, Changelog "Approver skip thẳng". Admin bypass flag check
        // — NV cuối vẫn cần ký thật để DaDuyet.
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var adminUser = await fix.CreateUserAsync("admin-f2-bypass@test.local", "Admin F2 Bypass",
                departmentId: null, roles: new[] { AppRoles.Admin });
            var userB = await fix.CreateUserAsync("userb-f2-bypass@test.local", "User B F2 Bypass",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userC = await fix.CreateUserAsync("userc-f2-bypass@test.local", "User C F2 Bypass",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var userD = await fix.CreateUserAsync("userd-f2-bypass@test.local", "User D F2 Bypass",
                departmentId: null, roles: new[] { AppRoles.CostControl });
            var (wf, _, _, _, _, _, _) = await SeedApproverF2WorkflowAsync(
                db, adminUser.Id, userB.Id, userC.Id, userD.Id,
                allowApproverSkipToFinalSlotS1L1: false);

            var pe = new PurchaseEvaluation
            {
                Id = Guid.NewGuid(),
                Type = PurchaseEvaluationType.DuyetNcc,
                Phase = PurchaseEvaluationPhase.ChoDuyet,
                MaPhieu = "PE-F2-003",
                TenGoiThau = "Approver F2 admin bypass",
                ProjectId = Guid.NewGuid(),
                DrafterUserId = Guid.NewGuid(),
                ApprovalWorkflowId = wf.Id,
                CurrentWorkflowStepIndex = 0,
                CurrentApprovalLevelOrder = 1,
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.ChoDuyet,
                actorUserId: adminUser.Id,
                actorRoles: new[] { AppRoles.Admin },
                decision: ApprovalDecision.Approve,
                comment: "admin duyệt thẳng",
                skipToFinal: true,
                ct: CancellationToken.None);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "L1 S23 t2: skip advance pointer, NV cuối duyệt thật");
            pe.CurrentWorkflowStepIndex.Should().Be(1, "Pointer advance tới lastStepIdx");
            pe.CurrentApprovalLevelOrder.Should().Be(2, "Pointer advance tới lastLevelMaxOrder");

            // Opinion logged (UPSERT trước skip)
            var opinions = await db.PurchaseEvaluationLevelOpinions
                .Where(o => o.PurchaseEvaluationId == pe.Id).ToListAsync();
            opinions.Should().HaveCount(1);

            // Changelog entry với "Approver skip thẳng"
            var changelogs = await db.PurchaseEvaluationChangelogs
                .Where(c => c.PurchaseEvaluationId == pe.Id).ToListAsync();
            changelogs.Should().Contain(c => c.ContextNote != null
                && c.ContextNote.Contains("Approver skip thẳng"));
        }
    }

    // ============ UAT S22+1: V2 actor scope guard cho Reject ============

    [Fact]
    public async Task Reject_NonApprover_V2_Throws_ForbiddenException()
    {
        // UAT S22+1 — actor không phải approver Cấp hiện tại + V2 pin + non-admin
        // → BE guard throw ForbiddenException. Mirror FE button disable logic.
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var (a1, a2) = await SeedApproversAsync(fix, "guard1");
            // SeedWorkflow Level 2 ApproverUserId = a2. Cấp hiện tại = Level 2.
            var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnToDrafterL2: true);
            var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid(), code: "PE-G-V2-001");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            // Outsider gọi Reject — không phải a2 (current Level approver).
            var outsider = await fix.CreateUserAsync(
                "outsider-guard@test.local", "Outsider Guard", departmentId: null,
                roles: new[] { AppRoles.CostControl });

            var act = async () => await svc.TransitionAsync(
                evaluation: pe,
                targetPhase: PurchaseEvaluationPhase.TraLai,
                actorUserId: outsider.Id,
                actorRoles: new[] { AppRoles.CostControl },
                decision: ApprovalDecision.Reject,
                comment: "outsider thử forge reject",
                returnMode: WorkflowReturnMode.Drafter,
                ct: CancellationToken.None);

            await act.Should().ThrowAsync<ForbiddenException>()
                .WithMessage("*Không phải lượt bạn*Trả lại / Từ chối*");
            pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "Guard chặn trước mutate phase");
        }
    }

}
