using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Office;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Hrm;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Office;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Application;

// Phase 11 P11-A Wave 4 (S42 2026-05-30) — test-after ApproveV2 wire WorkflowApps.
// Critical-algo state machine + UPSERT invariant cho LeaveOtApprovalFeatures.cs
// (LeaveRequest đầy đủ 8 case + OtRequest smoke 1 case).
//
// S56 #4 (2026-06-09): + Travel + Vehicle ApproveV2 smoke (TravelVehicleApprovalFeatures.cs) —
//   trước đây 0 test, cookie-cutter của Leave/Ot path. Mỗi module: Submit→Approve→DaDuyet
//   (happy path) + outsider (không phải ApproverUserId của cấp, không Admin) → Forbidden.
//   ApplicableType: TravelRequest=9 (prefix DT/CT) · VehicleBooking=7 (prefix DX/XE).
//   Travel/Vehicle KHÔNG trừ balance → không cần seed LeaveType.
//
// Handlers là CQRS MediatR — inject IApplicationDbContext + ICurrentUser + IDateTime
// trực tiếp (không qua service). Khác ContractWorkflowServiceApproveV2Tests (service
// wire 6 dep) — đây nhẹ hơn, instantiate handler + gọi Handle().
//
// Mirror LeaveOtApprovalFeatures.cs:250 ApproveLeaveRequestHandler. Status state:
//   Nhap=1, DaGuiDuyet=2, TraLai=3, TuChoi=4, DaDuyet=5.
// LevelOpinion UNIQUE composite (LeaveRequestId, ApprovalWorkflowLevelId).
public class WorkflowAppApproveV2Tests
{
    private static readonly DateTime FixedNow = new(2026, 5, 30, 8, 0, 0, DateTimeKind.Utc);

    private static (IdentityFixture fix, TestApplicationDbContext db, FixedDateTime clock) NewCtx()
    {
        var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var clock = new FixedDateTime(FixedNow);
        return (fix, db, clock);
    }

    private static TestCurrentUser AsUser(User u, params string[] roles)
        => new() { UserId = u.Id, FullName = u.FullName, Roles = roles ?? Array.Empty<string>() };

    // Seed 1 Bước × N Cấp LeaveRequest workflow. Trả về list Levels theo Order asc.
    private static async Task<(ApprovalWorkflow wf, List<ApprovalWorkflowLevel> levels)> SeedLeaveWorkflowAsync(
        TestApplicationDbContext db, params Guid[] approverUserIds)
    {
        var wf = new ApprovalWorkflow
        {
            Id = Guid.NewGuid(),
            Code = "QT-LR-001",
            Version = 1,
            Name = "Quy trình nghỉ phép test",
            ApplicableType = ApprovalWorkflowApplicableType.LeaveRequest,
            IsActive = true,
            IsUserSelectable = true,
        };
        var step = new ApprovalWorkflowStep
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowId = wf.Id,
            Order = 1,
            DepartmentId = null,
            Name = "Bước 1",
        };
        var levels = new List<ApprovalWorkflowLevel>();
        for (var i = 0; i < approverUserIds.Length; i++)
        {
            levels.Add(new ApprovalWorkflowLevel
            {
                Id = Guid.NewGuid(),
                ApprovalWorkflowStepId = step.Id,
                Order = i + 1,
                ApproverUserId = approverUserIds[i],
            });
        }
        db.ApprovalWorkflows.Add(wf);
        db.ApprovalWorkflowSteps.Add(step);
        db.ApprovalWorkflowLevels.AddRange(levels);
        await db.SaveChangesAsync(CancellationToken.None);
        return (wf, levels);
    }

    // leaveTypeId: chỉ cần seeded type thật khi đơn đi tới TERMINAL (DaDuyet) — nhánh đó
    // trừ phép insert LeaveBalance FK→LeaveTypes (Restrict). Test non-terminal để null (random).
    private static LeaveRequest BuildLeave(
        Guid requesterId,
        Guid? workflowId,
        WorkflowAppStatus status,
        int? currentLevel,
        Guid? leaveTypeId = null)
        => new()
        {
            Id = Guid.NewGuid(),
            RequesterUserId = requesterId,
            RequesterFullName = "Người tạo",
            LeaveTypeId = leaveTypeId ?? Guid.NewGuid(),
            StartDate = FixedNow.Date,
            EndDate = FixedNow.Date.AddDays(2),
            NumDays = 3,
            Reason = "Nghỉ việc riêng",
            Status = status,
            ApprovalWorkflowId = workflowId,
            CurrentApprovalLevelOrder = currentLevel,
        };

    // Seed 1 LeaveType (FK target cho trừ phép terminal). Trả entity dùng Id.
    private static async Task<LeaveType> SeedLeaveTypeAsync(TestApplicationDbContext db, string code, decimal daysPerYear)
    {
        var type = new LeaveType
        {
            Id = Guid.NewGuid(),
            Code = code,
            Name = $"Loại {code}",
            DaysPerYear = daysPerYear,
            IsPaid = true,
            IsActive = true,
        };
        db.LeaveTypes.Add(type);
        await db.SaveChangesAsync(CancellationToken.None);
        return type;
    }

    // ============ Case 1: Submit happy path ============

    [Fact]
    public async Task Submit_FromNhap_AdvancesToDaGuiDuyet_GeneratesMaDonTu()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c1@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-c1@test.local", "Approver", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.Nhap, currentLevel: null);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new SubmitLeaveRequestHandler(db, AsUser(requester), clock);
            await handler.Handle(new SubmitLeaveRequestCommand(leave.Id), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet);
            leave.CurrentApprovalLevelOrder.Should().Be(1);
            leave.RejectedFromStatus.Should().BeNull();
            leave.MaDonTu.Should().NotBeNullOrEmpty();
            leave.MaDonTu.Should().Be("DT/LR/2026/001", "prefix DT/LR + năm clock + seq D3");

            var seq = await db.WorkflowAppCodeSequences.FirstAsync(s => s.Prefix == "DT/LR/2026");
            seq.LastSeq.Should().Be(1);
        }
    }

    // ============ Case 2: Submit guards ============

    [Fact]
    public async Task Submit_WhenStatusNotNhapOrTraLai_ThrowsConflict()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c2a@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-c2a@test.local", "Approver", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new SubmitLeaveRequestHandler(db, AsUser(requester), clock);
            var act = async () => await handler.Handle(new SubmitLeaveRequestCommand(leave.Id), CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>().WithMessage("*Nháp hoặc Trả lại*");
        }
    }

    [Fact]
    public async Task Submit_WhenApprovalWorkflowIdNull_ThrowsConflict()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c2b@test.local", "Requester", null, Array.Empty<string>());

            var leave = BuildLeave(requester.Id, workflowId: null, WorkflowAppStatus.Nhap, currentLevel: null);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new SubmitLeaveRequestHandler(db, AsUser(requester), clock);
            var act = async () => await handler.Handle(new SubmitLeaveRequestCommand(leave.Id), CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>().WithMessage("*Chưa chọn quy trình duyệt*");
        }
    }

    // ============ Case 3: Approve advance (2-level, cấp 1) ============

    [Fact]
    public async Task Approve_FirstLevel_TwoLevel_AdvancesAndCreatesOneOpinion()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c3@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c3@test.local", "Approver 1", null, Array.Empty<string>());
            var ap2 = await fix.CreateUserAsync("ap2-c3@test.local", "Approver 2", null, Array.Empty<string>());
            var (wf, levels) = await SeedLeaveWorkflowAsync(db, ap1.Id, ap2.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new ApproveLeaveRequestHandler(db, AsUser(ap1), clock);
            await handler.Handle(new ApproveLeaveRequestCommand(leave.Id, "đồng ý"), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet, "còn Cấp 2 chưa terminal");
            leave.CurrentApprovalLevelOrder.Should().Be(2);

            var opinions = await db.LeaveRequestLevelOpinions.Where(o => o.LeaveRequestId == leave.Id).ToListAsync();
            opinions.Should().HaveCount(1);
            opinions[0].ApprovalWorkflowLevelId.Should().Be(levels[0].Id, "slot Cấp 1");
            opinions[0].Comment.Should().Be("đồng ý");
            opinions[0].SignedByUserId.Should().Be(ap1.Id);
        }
    }

    // ============ Case 4: Approve terminal (cấp cuối) ============

    [Fact]
    public async Task Approve_LastLevel_TransitionsToDaDuyet_ClearsCurrentLevel()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c4@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c4@test.local", "Approver 1", null, Array.Empty<string>());
            var ap2 = await fix.CreateUserAsync("ap2-c4@test.local", "Approver 2", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", 12m); // terminal trừ phép → cần LeaveType thật
            var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id, ap2.Id);

            // pin tại Cấp cuối (2)
            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 2, leaveTypeId: type.Id);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new ApproveLeaveRequestHandler(db, AsUser(ap2), clock);
            await handler.Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt cuối"), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.DaDuyet, "Cấp cuối → terminal");
            leave.CurrentApprovalLevelOrder.Should().BeNull();

            var opinions = await db.LeaveRequestLevelOpinions.Where(o => o.LeaveRequestId == leave.Id).ToListAsync();
            opinions.Should().HaveCount(1, "1 row cho slot Cấp 2");
        }
    }

    // ============ Case 5: Approve UPSERT invariant (re-sign same level) ============

    [Fact]
    public async Task Approve_SameLevelTwice_AdminReSign_DoesNotDuplicateRow_UpdatesLatestWrite()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c5@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c5@test.local", "Approver 1", null, Array.Empty<string>());
            var ap2 = await fix.CreateUserAsync("ap2-c5@test.local", "Approver 2", null, Array.Empty<string>());
            var admin = await fix.CreateUserAsync("admin-c5@test.local", "Quản trị", null, new[] { "Admin" });
            var (wf, levels) = await SeedLeaveWorkflowAsync(db, ap1.Id, ap2.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            // 1st approve Cấp 1 bởi ap1 → tạo row, advance lên Cấp 2
            await new ApproveLeaveRequestHandler(db, AsUser(ap1), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "ý kiến gốc"), CancellationToken.None);
            leave.CurrentApprovalLevelOrder.Should().Be(2);

            // Admin override re-sign Cấp 1: pin pointer trở lại Cấp 1 + admin duyệt lại
            leave.CurrentApprovalLevelOrder = 1;
            await db.SaveChangesAsync(CancellationToken.None);
            await new ApproveLeaveRequestHandler(db, AsUser(admin, "Admin"), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "admin ký lại"), CancellationToken.None);

            var opinions = await db.LeaveRequestLevelOpinions
                .Where(o => o.LeaveRequestId == leave.Id && o.ApprovalWorkflowLevelId == levels[0].Id)
                .ToListAsync();
            opinions.Should().HaveCount(1, "UNIQUE composite — KHÔNG tạo row thứ 2");
            opinions[0].Comment.Should().Be("admin ký lại", "latest-write-wins");
            opinions[0].SignedByUserId.Should().Be(admin.Id, "signer cập nhật người ký mới nhất");
        }
    }

    // ============ Case 6: Approve forbidden (outsider non-admin) ============

    [Fact]
    public async Task Approve_OutsiderNonAdmin_ThrowsForbidden_StateUnchanged()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c6@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c6@test.local", "Approver 1", null, Array.Empty<string>());
            var outsider = await fix.CreateUserAsync("out-c6@test.local", "Outsider", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new ApproveLeaveRequestHandler(db, AsUser(outsider), clock);
            var act = async () => await handler.Handle(new ApproveLeaveRequestCommand(leave.Id, "thử duyệt"), CancellationToken.None);

            await act.Should().ThrowAsync<ForbiddenException>().WithMessage("*Không phải người duyệt*");
            leave.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet, "guard chặn trước mutate");
            leave.CurrentApprovalLevelOrder.Should().Be(1);
            (await db.LeaveRequestLevelOpinions.CountAsync(o => o.LeaveRequestId == leave.Id))
                .Should().Be(0, "không tạo opinion khi bị chặn");
        }
    }

    // ============ Case 7: Approve empty comment → placeholder ============

    [Fact]
    public async Task Approve_EmptyComment_StoresPlaceholder()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c7@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c7@test.local", "Approver 1", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", 12m); // single-level → terminal → trừ phép cần LeaveType
            var (wf, levels) = await SeedLeaveWorkflowAsync(db, ap1.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, leaveTypeId: type.Id);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            await new ApproveLeaveRequestHandler(db, AsUser(ap1), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "   "), CancellationToken.None);

            var opinion = await db.LeaveRequestLevelOpinions
                .FirstAsync(o => o.LeaveRequestId == leave.Id && o.ApprovalWorkflowLevelId == levels[0].Id);
            opinion.Comment.Should().Be("(duyệt — không ý kiến)");
        }
    }

    // ============ Case 8a: Reject → TuChoi ============

    [Fact]
    public async Task Reject_FromDaGuiDuyet_TransitionsToTuChoi_ClearsCurrentLevel()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c8a@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c8a@test.local", "Approver 1", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            await new RejectLeaveRequestHandler(db, AsUser(ap1), clock)
                .Handle(new RejectLeaveRequestCommand(leave.Id, "không duyệt"), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.TuChoi);
            leave.CurrentApprovalLevelOrder.Should().BeNull();
        }
    }

    // ============ Case 8b: Return → TraLai + RejectedFromStatus ============

    [Fact]
    public async Task Return_FromDaGuiDuyet_TransitionsToTraLai_SetsRejectedFromStatus()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-c8b@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-c8b@test.local", "Approver 1", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            await new ReturnLeaveRequestHandler(db, AsUser(ap1), clock)
                .Handle(new ReturnLeaveRequestCommand(leave.Id, "sửa lại"), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.TraLai);
            leave.RejectedFromStatus.Should().Be(WorkflowAppStatus.DaGuiDuyet);
            leave.CurrentApprovalLevelOrder.Should().BeNull();
        }
    }

    // ============ Smoke OtRequest: mirror cookie-cutter — terminal happy path ============

    [Fact]
    public async Task OtRequest_Submit_Then_Approve_SingleLevel_ReachesDaDuyet()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-ot@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-ot@test.local", "Approver", null, Array.Empty<string>());

            var wf = new ApprovalWorkflow
            {
                Id = Guid.NewGuid(),
                Code = "QT-OT-001",
                Version = 1,
                Name = "Quy trình OT test",
                ApplicableType = ApprovalWorkflowApplicableType.OtRequest,
                IsActive = true,
                IsUserSelectable = true,
            };
            var step = new ApprovalWorkflowStep
            {
                Id = Guid.NewGuid(),
                ApprovalWorkflowId = wf.Id,
                Order = 1,
                DepartmentId = null,
                Name = "Bước 1",
            };
            var lvl = new ApprovalWorkflowLevel
            {
                Id = Guid.NewGuid(),
                ApprovalWorkflowStepId = step.Id,
                Order = 1,
                ApproverUserId = approver.Id,
            };
            db.ApprovalWorkflows.Add(wf);
            db.ApprovalWorkflowSteps.Add(step);
            db.ApprovalWorkflowLevels.Add(lvl);

            var ot = new OtRequest
            {
                Id = Guid.NewGuid(),
                RequesterUserId = requester.Id,
                RequesterFullName = "Người tạo",
                OtDate = FixedNow.Date,
                StartTime = TimeSpan.FromHours(18),
                EndTime = TimeSpan.FromHours(21),
                Hours = 3,
                Reason = "Tăng ca giao hàng",
                Status = WorkflowAppStatus.Nhap,
                ApprovalWorkflowId = wf.Id,
            };
            db.OtRequests.Add(ot);
            await db.SaveChangesAsync(CancellationToken.None);

            await new SubmitOtRequestHandler(db, AsUser(requester), clock)
                .Handle(new SubmitOtRequestCommand(ot.Id), CancellationToken.None);
            ot.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet);
            ot.CurrentApprovalLevelOrder.Should().Be(1);
            ot.MaDonTu.Should().Be("DT/OT/2026/001");

            await new ApproveOtRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveOtRequestCommand(ot.Id, null), CancellationToken.None);
            ot.Status.Should().Be(WorkflowAppStatus.DaDuyet);
            ot.CurrentApprovalLevelOrder.Should().BeNull();

            var opinions = await db.OtRequestLevelOpinions.Where(o => o.OtRequestId == ot.Id).ToListAsync();
            opinions.Should().HaveCount(1);
            opinions[0].Comment.Should().Be("(duyệt — không ý kiến)");
        }
    }

    // ============ SetWorkflow (P11-A fix S42): pin quy trình cho draft ============
    // Endpoint riêng /workflow — KHÔNG validate field khác (fix FE bug PUT /{id} partial → 400).

    [Fact]
    public async Task SetWorkflow_OnDraft_SetsApprovalWorkflowId_NoStatusChange()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-sw1@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-sw1@test.local", "Approver", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            var leave = BuildLeave(requester.Id, workflowId: null, WorkflowAppStatus.Nhap, currentLevel: null);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            await new SetLeaveRequestWorkflowHandler(db, AsUser(requester), clock)
                .Handle(new SetLeaveRequestWorkflowCommand(leave.Id, wf.Id), CancellationToken.None);

            leave.ApprovalWorkflowId.Should().Be(wf.Id);
            leave.Status.Should().Be(WorkflowAppStatus.Nhap, "set workflow KHÔNG đổi trạng thái");
        }
    }

    [Fact]
    public async Task SetWorkflow_WrongApplicableType_ThrowsConflict_DoesNotPin()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-sw2@test.local", "Requester", null, Array.Empty<string>());

            // Workflow loại OtRequest (=6) — KHÔNG khớp LeaveRequest (=5)
            var otWf = new ApprovalWorkflow
            {
                Id = Guid.NewGuid(),
                Code = "QT-OT-X",
                Version = 1,
                Name = "OT workflow",
                ApplicableType = ApprovalWorkflowApplicableType.OtRequest,
                IsActive = true,
                IsUserSelectable = true,
            };
            db.ApprovalWorkflows.Add(otWf);

            var leave = BuildLeave(requester.Id, workflowId: null, WorkflowAppStatus.Nhap, currentLevel: null);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await new SetLeaveRequestWorkflowHandler(db, AsUser(requester), clock)
                .Handle(new SetLeaveRequestWorkflowCommand(leave.Id, otWf.Id), CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>().WithMessage("*Đơn nghỉ phép*");
            leave.ApprovalWorkflowId.Should().BeNull("guard chặn trước khi pin");
        }
    }

    [Fact]
    public async Task SetWorkflow_WhenAlreadySubmitted_ThrowsConflict()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-sw3@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-sw3@test.local", "Approver", null, Array.Empty<string>());
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await new SetLeaveRequestWorkflowHandler(db, AsUser(requester), clock)
                .Handle(new SetLeaveRequestWorkflowCommand(leave.Id, wf.Id), CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>().WithMessage("*Nháp hoặc Trả lại*");
        }
    }

    // =====================================================================
    // S56 #4 — Travel + Vehicle ApproveV2 smoke (cookie-cutter mirror Leave/Ot)
    // =====================================================================

    // Seed 1 Bước × N Cấp workflow cho ApplicableType bất kỳ (Travel=9 / Vehicle=7).
    private static async Task<ApprovalWorkflow> SeedWorkflowForTypeAsync(
        TestApplicationDbContext db, ApprovalWorkflowApplicableType type, string code, params Guid[] approverUserIds)
    {
        var wf = new ApprovalWorkflow
        {
            Id = Guid.NewGuid(),
            Code = code,
            Version = 1,
            Name = $"Quy trình {code}",
            ApplicableType = type,
            IsActive = true,
            IsUserSelectable = true,
        };
        var step = new ApprovalWorkflowStep
        {
            Id = Guid.NewGuid(),
            ApprovalWorkflowId = wf.Id,
            Order = 1,
            DepartmentId = null,
            Name = "Bước 1",
        };
        var levels = new List<ApprovalWorkflowLevel>();
        for (var i = 0; i < approverUserIds.Length; i++)
        {
            levels.Add(new ApprovalWorkflowLevel
            {
                Id = Guid.NewGuid(),
                ApprovalWorkflowStepId = step.Id,
                Order = i + 1,
                ApproverUserId = approverUserIds[i],
            });
        }
        db.ApprovalWorkflows.Add(wf);
        db.ApprovalWorkflowSteps.Add(step);
        db.ApprovalWorkflowLevels.AddRange(levels);
        await db.SaveChangesAsync(CancellationToken.None);
        return wf;
    }

    private static TravelRequest BuildTravel(Guid requesterId, Guid? workflowId)
        => new()
        {
            Id = Guid.NewGuid(),
            RequesterUserId = requesterId,
            RequesterFullName = "Người tạo",
            Destination = "Hà Nội",
            StartDate = FixedNow.Date,
            EndDate = FixedNow.Date.AddDays(2),
            NumDays = 3,
            Purpose = "Khảo sát công trường",
            EstimatedCost = 5_000_000m,
            Status = WorkflowAppStatus.Nhap,
            ApprovalWorkflowId = workflowId,
        };

    private static VehicleBooking BuildVehicle(Guid requesterId, Guid? workflowId)
        => new()
        {
            Id = Guid.NewGuid(),
            RequesterUserId = requesterId,
            RequesterFullName = "Người tạo",
            VehicleLicense = "30A-12345",
            VehicleName = "Innova 7 chỗ",
            StartAt = FixedNow,
            EndAt = FixedNow.AddHours(8),
            Destination = "TP.HCM",
            Purpose = "Đón đối tác",
            DriverName = "Anh Tài",
            Status = WorkflowAppStatus.Nhap,
            ApprovalWorkflowId = workflowId,
        };

    // ---- Travel: Submit → Approve single-level → DaDuyet (happy path) ----

    [Fact]
    public async Task TravelRequest_Submit_Then_Approve_SingleLevel_ReachesDaDuyet()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-tr@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-tr@test.local", "Approver", null, Array.Empty<string>());
            var wf = await SeedWorkflowForTypeAsync(
                db, ApprovalWorkflowApplicableType.TravelRequest, "QT-TR-001", approver.Id);

            var tr = BuildTravel(requester.Id, wf.Id);
            db.TravelRequests.Add(tr);
            await db.SaveChangesAsync(CancellationToken.None);

            await new SubmitTravelRequestHandler(db, AsUser(requester), clock)
                .Handle(new SubmitTravelRequestCommand(tr.Id), CancellationToken.None);
            tr.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet);
            tr.CurrentApprovalLevelOrder.Should().Be(1);
            tr.MaDonTu.Should().Be("DT/CT/2026/001", "prefix DT/CT + năm + seq D3");

            await new ApproveTravelRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveTravelRequestCommand(tr.Id, "duyệt công tác"), CancellationToken.None);
            tr.Status.Should().Be(WorkflowAppStatus.DaDuyet, "single-level → terminal");
            tr.CurrentApprovalLevelOrder.Should().BeNull();

            var opinions = await db.TravelRequestLevelOpinions.Where(o => o.TravelRequestId == tr.Id).ToListAsync();
            opinions.Should().HaveCount(1);
            opinions[0].Comment.Should().Be("duyệt công tác");
            opinions[0].SignedByUserId.Should().Be(approver.Id);
        }
    }

    // ---- Travel: outsider (not approver, not Admin) → Forbidden, state unchanged ----

    [Fact]
    public async Task TravelRequest_Approve_OutsiderNonAdmin_ThrowsForbidden_StateUnchanged()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-tr2@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-tr2@test.local", "Approver", null, Array.Empty<string>());
            var outsider = await fix.CreateUserAsync("out-tr2@test.local", "Outsider", null, Array.Empty<string>());
            var wf = await SeedWorkflowForTypeAsync(
                db, ApprovalWorkflowApplicableType.TravelRequest, "QT-TR-002", approver.Id);

            var tr = BuildTravel(requester.Id, wf.Id);
            tr.Status = WorkflowAppStatus.DaGuiDuyet;
            tr.CurrentApprovalLevelOrder = 1;
            db.TravelRequests.Add(tr);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await new ApproveTravelRequestHandler(db, AsUser(outsider), clock)
                .Handle(new ApproveTravelRequestCommand(tr.Id, "thử duyệt"), CancellationToken.None);

            await act.Should().ThrowAsync<ForbiddenException>().WithMessage("*Không phải người duyệt*");
            tr.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet, "guard chặn trước mutate");
            tr.CurrentApprovalLevelOrder.Should().Be(1);
            (await db.TravelRequestLevelOpinions.CountAsync(o => o.TravelRequestId == tr.Id))
                .Should().Be(0, "không tạo opinion khi bị chặn");
        }
    }

    // ---- Vehicle: Submit → Approve single-level → DaDuyet (happy path) ----

    [Fact]
    public async Task VehicleBooking_Submit_Then_Approve_SingleLevel_ReachesDaDuyet()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-vb@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-vb@test.local", "Approver", null, Array.Empty<string>());
            var wf = await SeedWorkflowForTypeAsync(
                db, ApprovalWorkflowApplicableType.VehicleBooking, "QT-VB-001", approver.Id);

            var vb = BuildVehicle(requester.Id, wf.Id);
            db.VehicleBookings.Add(vb);
            await db.SaveChangesAsync(CancellationToken.None);

            await new SubmitVehicleBookingHandler(db, AsUser(requester), clock)
                .Handle(new SubmitVehicleBookingCommand(vb.Id), CancellationToken.None);
            vb.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet);
            vb.CurrentApprovalLevelOrder.Should().Be(1);
            vb.MaDonTu.Should().Be("DX/XE/2026/001", "prefix DX/XE + năm + seq D3");

            await new ApproveVehicleBookingHandler(db, AsUser(approver), clock)
                .Handle(new ApproveVehicleBookingCommand(vb.Id, null), CancellationToken.None);
            vb.Status.Should().Be(WorkflowAppStatus.DaDuyet, "single-level → terminal");
            vb.CurrentApprovalLevelOrder.Should().BeNull();

            var opinions = await db.VehicleBookingLevelOpinions.Where(o => o.VehicleBookingId == vb.Id).ToListAsync();
            opinions.Should().HaveCount(1);
            opinions[0].Comment.Should().Be("(duyệt — không ý kiến)", "comment null → placeholder");
            opinions[0].SignedByUserId.Should().Be(approver.Id);
        }
    }

    // ---- Vehicle: outsider (not approver, not Admin) → Forbidden, state unchanged ----

    [Fact]
    public async Task VehicleBooking_Approve_OutsiderNonAdmin_ThrowsForbidden_StateUnchanged()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-vb2@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-vb2@test.local", "Approver", null, Array.Empty<string>());
            var outsider = await fix.CreateUserAsync("out-vb2@test.local", "Outsider", null, Array.Empty<string>());
            var wf = await SeedWorkflowForTypeAsync(
                db, ApprovalWorkflowApplicableType.VehicleBooking, "QT-VB-002", approver.Id);

            var vb = BuildVehicle(requester.Id, wf.Id);
            vb.Status = WorkflowAppStatus.DaGuiDuyet;
            vb.CurrentApprovalLevelOrder = 1;
            db.VehicleBookings.Add(vb);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await new ApproveVehicleBookingHandler(db, AsUser(outsider), clock)
                .Handle(new ApproveVehicleBookingCommand(vb.Id, "thử duyệt"), CancellationToken.None);

            await act.Should().ThrowAsync<ForbiddenException>().WithMessage("*Không phải người duyệt*");
            vb.Status.Should().Be(WorkflowAppStatus.DaGuiDuyet, "guard chặn trước mutate");
            vb.CurrentApprovalLevelOrder.Should().Be(1);
            (await db.VehicleBookingLevelOpinions.CountAsync(o => o.VehicleBookingId == vb.Id))
                .Should().Be(0, "không tạo opinion khi bị chặn");
        }
    }
}
