using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Hrm;
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-B Wave 3 (S43 2026-05-30) — test-after, critical-algo (financial-ish trừ phép).
// Cover deduction hook trong ApproveLeaveRequestHandler terminal branch (LeaveOtApprovalFeatures.cs:344)
// + LeaveBalanceFeatures.cs query lazy-merge + AdjustLeaveBalance upsert.
//
// CHỐT theo CODE (single source of truth):
//   - Deduct CHỈ ở nhánh terminal DaDuyet (CurrentApprovalLevelOrder == allLevels.Count).
//     Advance level KHÔNG trừ. Reject/Return KHÔNG trừ.
//   - UPSERT theo UNIQUE (UserId, LeaveTypeId, Year). Auto-create EntitledDays=LeaveType.DaysPerYear,
//     UsedDays=0, AdjustmentDays=0 nếu chưa có row. UsedDays += NumDays.
//   - Year = StartDate.Year (KHÔNG phải EndDate / clock năm).
//   - KHÔNG validate âm → Used > Entitled cho phép (Remaining < 0, không throw).
//   - Query Remaining = EntitledDays + AdjustmentDays − UsedDays (COMPUTED ở DTO).
//     Lazy: chưa có row → synthesize Entitled=DaysPerYear, Used=0, Adjustment=0.
//
// ⚠️ Pre-existing failure REPORT (S42 template WorkflowAppApproveV2Tests.cs): hook Wave 1 mới
//    làm 2 test terminal cũ FK-fail (BuildLeave LeaveTypeId=Guid.NewGuid() → LeaveBalance insert
//    FK→LeaveTypes fail). Fix tại file đó: seed LeaveType + truyền Id. KHÔNG phải prod bug —
//    prod đơn nghỉ luôn pin LeaveType thật.
//
// FK note: LeaveBalance → LeaveType Restrict (LeaveBalanceConfiguration). MỌI đơn nghỉ test
//    terminal PHẢI seed 1 LeaveType row + LeaveRequest.LeaveTypeId = type.Id đó.
//
// ⚠️ S56 #3 (lost-update fix) — ĐỌC LẠI BALANCE PHẢI AsNoTracking(): handler terminal nay
//    increment UsedDays qua db.LeaveBalances.Where(...).ExecuteUpdateAsync(server-side, BYPASS
//    change tracker). Instance bal tracked (Add ở STEP1 hoặc pre-seed cùng context) GIỮ UsedDays
//    PRE-increment. Re-read mặc định trả tracked-stale → assert sai. Mọi assert post-deduction
//    dùng .AsNoTracking() (hoặc ChangeTracker.Clear()) để đọc giá trị DB thật. DB end-state đúng:
//    UsedDays += NumDays; row tự tạo Entitled=DaysPerYear/Used=0/Adjustment=0/Year=StartDate.Year.
public class LeaveBalanceTests
{
    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 LeaveType row (Restrict FK target). Trả entity để dùng Id cho LeaveRequest.LeaveTypeId.
    private static async Task<LeaveType> SeedLeaveTypeAsync(
        TestApplicationDbContext db, string code, decimal daysPerYear, bool isActive = true, bool isPaid = true)
    {
        var type = new LeaveType
        {
            Id = Guid.NewGuid(),
            Code = code,
            Name = $"Loại {code}",
            DaysPerYear = daysPerYear,
            IsPaid = isPaid,
            IsActive = isActive,
        };
        db.LeaveTypes.Add(type);
        await db.SaveChangesAsync(CancellationToken.None);
        return type;
    }

    // Seed 1 Bước × N Cấp LeaveRequest workflow (mirror WorkflowAppApproveV2Tests). 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-BAL",
            Version = 1,
            Name = "Quy trình nghỉ phép test balance",
            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);
    }

    // BuildLeave với LeaveTypeId explicit (KHÁC template — bắt buộc seeded type cho FK trừ phép).
    private static LeaveRequest BuildLeave(
        Guid requesterId,
        Guid leaveTypeId,
        Guid? workflowId,
        WorkflowAppStatus status,
        int? currentLevel,
        decimal numDays = 3,
        DateTime? startDate = null)
    {
        var start = startDate ?? FixedNow.Date;
        return new LeaveRequest
        {
            Id = Guid.NewGuid(),
            RequesterUserId = requesterId,
            RequesterFullName = "Người tạo",
            LeaveTypeId = leaveTypeId,
            StartDate = start,
            EndDate = start.AddDays((double)numDays - 1),
            NumDays = numDays,
            Reason = "Nghỉ việc riêng",
            Status = status,
            ApprovalWorkflowId = workflowId,
            CurrentApprovalLevelOrder = currentLevel,
        };
    }

    // ============ Case 1: Deduct single-level — tạo balance row mới ============

    [Fact]
    public async Task Approve_LastLevel_DeductsLeave_CreatesNewBalanceRow_FromDaysPerYear()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-b1@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-b1@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            // single-level, pin tại cấp cuối (=1), NumDays=3
            var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 3);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt"), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.DaDuyet);
            leave.CurrentApprovalLevelOrder.Should().BeNull();

            // S56 #3: re-read FRESH (ExecuteUpdateAsync bypass tracker → row tracked giữ Used=0).
            var bal = await db.LeaveBalances.AsNoTracking()
                .SingleAsync(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id);
            bal.Year.Should().Be(2026, "Year = StartDate.Year");
            bal.EntitledDays.Should().Be(12m, "auto-create từ LeaveType.DaysPerYear");
            bal.UsedDays.Should().Be(3m, "UsedDays += NumDays (đọc DB thật qua AsNoTracking)");
            bal.AdjustmentDays.Should().Be(0m);
            (bal.EntitledDays + bal.AdjustmentDays - bal.UsedDays).Should().Be(9m, "Remaining = 12 + 0 − 3");
        }
    }

    // ============ Case 2: Deduct only at terminal (multi-level) — chỉ trừ 1 lần ============

    [Fact]
    public async Task Approve_MultiLevel_NoDeductAtIntermediate_DeductsOnceAtTerminal()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-b2@test.local", "Requester", null, Array.Empty<string>());
            var ap1 = await fix.CreateUserAsync("ap1-b2@test.local", "Approver 1", null, Array.Empty<string>());
            var ap2 = await fix.CreateUserAsync("ap2-b2@test.local", "Approver 2", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id, ap2.Id);

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

            // Cấp 1 duyệt → advance, CHƯA terminal → KHÔNG có balance row
            await new ApproveLeaveRequestHandler(db, AsUser(ap1), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "ok cấp 1"), CancellationToken.None);
            leave.CurrentApprovalLevelOrder.Should().Be(2);
            (await db.LeaveBalances.CountAsync(b => b.UserId == requester.Id))
                .Should().Be(0, "advance level KHÔNG trừ phép");

            // Cấp 2 (cuối) duyệt → terminal → trừ 1 lần
            await new ApproveLeaveRequestHandler(db, AsUser(ap2), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "ok cấp cuối"), CancellationToken.None);
            leave.Status.Should().Be(WorkflowAppStatus.DaDuyet);

            // S56 #3: AsNoTracking để đọc UsedDays sau server-side increment.
            var balances = await db.LeaveBalances.AsNoTracking()
                .Where(b => b.UserId == requester.Id).ToListAsync();
            balances.Should().HaveCount(1, "chỉ 1 row, trừ đúng 1 lần ở terminal");
            balances[0].UsedDays.Should().Be(4m);
        }
    }

    // ============ Case 3: Accumulate existing balance — KHÔNG tạo row mới ============

    [Fact]
    public async Task Approve_LastLevel_AccumulatesExistingBalance_SameRow()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-b3@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-b3@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            // Pre-seed balance đã dùng 5 ngày cùng (User, Type, Year=2026)
            db.LeaveBalances.Add(new LeaveBalance
            {
                Id = Guid.NewGuid(),
                UserId = requester.Id,
                LeaveTypeId = type.Id,
                Year = 2026,
                EntitledDays = 12m,
                UsedDays = 5m,
                AdjustmentDays = 0m,
                CreatedAt = FixedNow,
            });
            var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 2);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt"), CancellationToken.None);

            // S56 #3: pre-seed row tracked + handler STEP1 re-fetch → tracked-stale Used=5. AsNoTracking
            // ép đọc DB thật (server-side increment đã +2).
            var balances = await db.LeaveBalances.AsNoTracking()
                .Where(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id && b.Year == 2026).ToListAsync();
            balances.Should().HaveCount(1, "UNIQUE (User,Type,Year) — accumulate KHÔNG tạo row mới");
            balances[0].UsedDays.Should().Be(7m, "5 + 2 cộng dồn (đọc DB thật qua AsNoTracking)");
        }
    }

    // ============ Case 4: Negative allowed (allow+warn policy) — KHÔNG throw ============

    [Fact]
    public async Task Approve_LastLevel_OverEntitled_AllowsNegativeRemaining_NoThrow()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-b4@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-b4@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            // NumDays=20 > Entitled 12 → Remaining âm, KHÔNG được throw
            var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 20);
            db.LeaveRequests.Add(leave);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt vượt"), CancellationToken.None);

            await act.Should().NotThrowAsync("policy allow+warn — KHÔNG chặn vượt quota");
            leave.Status.Should().Be(WorkflowAppStatus.DaDuyet);

            // S56 #3: AsNoTracking đọc UsedDays sau increment server-side.
            var bal = await db.LeaveBalances.AsNoTracking()
                .SingleAsync(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id);
            bal.UsedDays.Should().Be(20m);
            (bal.EntitledDays + bal.AdjustmentDays - bal.UsedDays).Should().Be(-8m, "Remaining = 12 − 20 = −8");
        }
    }

    // ============ Case 4b (S56 #3): hai đơn terminal cùng (User,Type,Year) → UsedDays CỘNG DỒN ============
    // Chứng minh ExecuteUpdateAsync increment server-side (b.UsedDays + NumDays) = ACCUMULATE,
    // KHÔNG overwrite. Đây là invariant race-free: 2 lượt trừ phép tuần tự cộng dồn đúng tổng
    // (cùng cơ chế bảo vệ lost-update khi 2 lượt duyệt cuối song song serialize qua row).

    [Fact]
    public async Task TwoSeparateRequests_BothTerminal_UsedDaysAccumulates_NotOverwrites()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-acc@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-acc@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

            // Đơn 1: 3 ngày → terminal (tạo row, Used 0→3).
            var leave1 = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 3);
            db.LeaveRequests.Add(leave1);
            await db.SaveChangesAsync(CancellationToken.None);
            await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave1.Id, "đơn 1"), CancellationToken.None);

            // Đơn 2: 5 ngày → terminal (cùng UNIQUE slot, Used 3→8 — accumulate qua server-side increment).
            var leave2 = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 5);
            db.LeaveRequests.Add(leave2);
            await db.SaveChangesAsync(CancellationToken.None);
            await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave2.Id, "đơn 2"), CancellationToken.None);

            var balances = await db.LeaveBalances.AsNoTracking()
                .Where(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id && b.Year == 2026).ToListAsync();
            balances.Should().HaveCount(1, "1 row duy nhất (UNIQUE User,Type,Year)");
            balances[0].UsedDays.Should().Be(8m, "3 + 5 cộng dồn — increment KHÔNG ghi đè (race-free lost-update fix)");
        }
    }

    // ============ Case 4c (S56 #3): exactly-once — re-approve đơn DaDuyet → Conflict, KHÔNG trừ lần 2 ============
    // Early guard Status != DaGuiDuyet (LeaveOtApprovalFeatures.cs:296) chặn re-approve tuần tự.
    // Balance KHÔNG bị trừ thêm — chốt deduction chạy đúng MỘT lần dù gọi Approve 2 lần.

    [Fact]
    public async Task Approve_AlreadyDaDuyet_ReApprove_ThrowsConflict_NoDoubleDeduct()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-once@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-once@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

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

            // Lần 1: terminal → trừ 3, Status=DaDuyet.
            await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt cuối"), CancellationToken.None);
            leave.Status.Should().Be(WorkflowAppStatus.DaDuyet);

            // Lần 2 (cùng approver, đơn đã DaDuyet) → early guard chặn → ConflictException.
            var reApprove = async () => await new ApproveLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt lại"), CancellationToken.None);
            await reApprove.Should().ThrowAsync<ConflictException>()
                .WithMessage("*Đã gửi duyệt*", "guard Status != DaGuiDuyet chặn re-approve → exactly-once");

            // Balance KHÔNG bị trừ lần 2 — vẫn đúng 3 (không phải 6).
            var bal = await db.LeaveBalances.AsNoTracking()
                .SingleAsync(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id && b.Year == 2026);
            bal.UsedDays.Should().Be(3m, "deduction chạy đúng 1 lần — re-approve bị guard chặn, không double-count");
        }
    }

    // ============ Case 5a: Reject KHÔNG trừ ============

    [Fact]
    public async Task Reject_DoesNotDeductLeave()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-b5a@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-b5a@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

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

            await new RejectLeaveRequestHandler(db, AsUser(approver), clock)
                .Handle(new RejectLeaveRequestCommand(leave.Id, "từ chối"), CancellationToken.None);

            leave.Status.Should().Be(WorkflowAppStatus.TuChoi);
            (await db.LeaveBalances.CountAsync(b => b.UserId == requester.Id))
                .Should().Be(0, "chỉ terminal DaDuyet mới trừ — TuChoi KHÔNG");
        }
    }

    // ============ Case 5b: Return KHÔNG trừ ============

    [Fact]
    public async Task Return_DoesNotDeductLeave()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-b5b@test.local", "Requester", null, Array.Empty<string>());
            var approver = await fix.CreateUserAsync("ap-b5b@test.local", "Approver", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);

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

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

            leave.Status.Should().Be(WorkflowAppStatus.TraLai);
            (await db.LeaveBalances.CountAsync(b => b.UserId == requester.Id))
                .Should().Be(0, "TraLai KHÔNG trừ phép");
        }
    }

    // ============ Case 6: GetMyLeaveBalances lazy — synthesize default cho mọi active type ============

    [Fact]
    public async Task GetMyLeaveBalances_NoBalanceRows_SynthesizesDefaultsForActiveTypes()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var user = await fix.CreateUserAsync("req-b6@test.local", "User", null, Array.Empty<string>());
            await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);
            await SeedLeaveTypeAsync(db, "SICK", daysPerYear: 30m);
            // 1 type inactive — KHÔNG xuất hiện trong kết quả
            await SeedLeaveTypeAsync(db, "RETIRED", daysPerYear: 5m, isActive: false);

            var result = await new GetMyLeaveBalancesHandler(db, AsUser(user))
                .Handle(new GetMyLeaveBalancesQuery(2026), CancellationToken.None);

            result.Should().HaveCount(2, "chỉ 2 type active, inactive bị loại");
            result.Should().OnlyContain(d => d.UsedDays == 0m, "chưa có row → Used=0");
            // ordered by Code: ANNUAL trước SICK
            var annual = result.Single(d => d.Code == "ANNUAL");
            annual.EntitledDays.Should().Be(12m, "synthesize từ DaysPerYear");
            annual.RemainingDays.Should().Be(12m, "Remaining = 12 + 0 − 0");
            var sick = result.Single(d => d.Code == "SICK");
            sick.EntitledDays.Should().Be(30m);
            sick.RemainingDays.Should().Be(30m);
        }
    }

    // ============ Case 7: AdjustLeaveBalance upsert — tạo row khi chưa có + query phản ánh ============

    [Fact]
    public async Task AdjustLeaveBalance_NoRow_CreatesRow_QueryReflectsRemaining()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var admin = await fix.CreateUserAsync("admin-b7@test.local", "Quản trị", null, new[] { "Admin" });
            var target = await fix.CreateUserAsync("tgt-b7@test.local", "Nhân viên", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);

            // Admin upsert: EntitledDays=15 (override DaysPerYear), AdjustmentDays=2 (carry-over)
            await new AdjustLeaveBalanceHandler(db, AsUser(admin, "Admin"), clock)
                .Handle(new AdjustLeaveBalanceCommand(target.Id, type.Id, 2026, EntitledDays: 15m, AdjustmentDays: 2m),
                    CancellationToken.None);

            var bal = await db.LeaveBalances
                .SingleAsync(b => b.UserId == target.Id && b.LeaveTypeId == type.Id && b.Year == 2026);
            bal.EntitledDays.Should().Be(15m, "override DaysPerYear bằng giá trị admin nhập");
            bal.AdjustmentDays.Should().Be(2m);
            bal.UsedDays.Should().Be(0m);

            // Query lại — Remaining phản ánh: 15 + 2 − 0 = 17
            var result = await new GetUserLeaveBalancesHandler(db)
                .Handle(new GetUserLeaveBalancesQuery(target.Id, 2026), CancellationToken.None);
            var annual = result.Single(d => d.Code == "ANNUAL");
            annual.EntitledDays.Should().Be(15m);
            annual.AdjustmentDays.Should().Be(2m);
            annual.RemainingDays.Should().Be(17m, "Remaining = Entitled 15 + Adjustment 2 − Used 0");
        }
    }

    // ============ Guard P11-B: LeaveTypeId phải tồn tại (chặn 500 FK-fail lúc duyệt cuối) ============

    [Fact]
    public async Task CreateLeaveRequest_BogusLeaveTypeId_ThrowsConflict_NoRowAdded()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-guard@test.local", "Requester", null, Array.Empty<string>());
            // KHÔNG seed LeaveType → LeaveTypeId random là bogus.
            var act = async () => await new CreateLeaveRequestHandler(db, AsUser(requester), clock)
                .Handle(new CreateLeaveRequestCommand(Guid.NewGuid(), FixedNow.Date, FixedNow.Date.AddDays(1),
                    1m, "nghỉ", ApprovalWorkflowId: null), CancellationToken.None);

            await act.Should().ThrowAsync<ConflictException>().WithMessage("*Loại phép không tồn tại*");
            (await db.LeaveRequests.CountAsync()).Should().Be(0, "guard chặn trước khi Add");
        }
    }

    [Fact]
    public async Task CreateLeaveRequest_ValidLeaveType_Succeeds()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync("req-guard2@test.local", "Requester", null, Array.Empty<string>());
            var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m);

            var id = await new CreateLeaveRequestHandler(db, AsUser(requester), clock)
                .Handle(new CreateLeaveRequestCommand(type.Id, FixedNow.Date, FixedNow.Date.AddDays(1),
                    1m, "nghỉ", ApprovalWorkflowId: null), CancellationToken.None);

            id.Should().NotBeEmpty();
            (await db.LeaveRequests.CountAsync(x => x.Id == id)).Should().Be(1);
        }
    }
}
