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

namespace SolutionErp.Infrastructure.Tests.Services;

// ===== NEW (Mig 54 2026-06-18 anh Kiệt FDC) — ① GIÁ CHỐT ApplyApprovedPriceOnFinalize =====
// PurchaseEvaluationWorkflowService.ApplyApprovedPriceOnFinalize (line 908-923 prod) +
// 2 call-site nhánh DaDuyet trong ApproveV2Async: (a) terminal normal advance (line 885)
// và (b) CCM-delegation finalize (line 853 — cover ở PeCcmThresholdFinalizeTests).
// Test theo CODE đã land (S34 rule — KHÔNG touch production).
//
// Contract ApplyApprovedPriceOnFinalize(evaluation, isSystem, amount, source, maxAmount=null):
//   - amount null + !isSystem → ConflictException ("Chọn 1 giá chốt...")
//   - amount null + isSystem  → return im lặng (auto-approve hệ thống MIỄN chọn giá)
//   - source null/rác (∉ {Ncc,ProMin,ProMax,Ccm,ProMinMax}) → ConflictException
//   - hợp lệ → set evaluation.ApprovedPriceAmount/Source
//
// ===== NEW (S117 2026-07-14 anh Kiệt FDC) — "Duyệt theo KHOẢNG" (source="ProMinMax") =====
// Khi PRO có cả Min & Max, người duyệt cấp cuối chọn CẢ HAI làm giá chốt → Amount=Min, MaxAmount=Max.
// Prod: ApplyApprovedPriceOnFinalize +param `decimal? maxAmount` (line 955-984); ValidApprovedPriceSources
// +"ProMinMax"; TransitionAsync/ApproveV2Async +param approvedPriceMaxAmount (SAU source, TRƯỚC applyLevelFinalize).
//   - source="ProMinMax" + maxAmount null      → Conflict "*phải có cả giá Min và giá Max*"
//   - source="ProMinMax" + amount > maxAmount   → Conflict "*không được lớn hơn giá Max*" (boundary `>`: min==max OK)
//   - source ĐƠN (Ncc/ProMin/ProMax/Ccm) + maxAmount≠null → Conflict "*kèm giá Max*" (wire-bug guard)
//   - happy → Amount=amount(Min), Source=source, MaxAmount = (source=="ProMinMax" ? maxAmount : null)
//   Bất-biến: ApprovedPriceMaxAmount != null ⟺ ApprovedPriceSource == "ProMinMax".
// Financial-critical → test-before merge. NEW test đi qua reachable terminal path (mirror case 1), KHÔNG
// gọi thẳng private helper. InvokeApply reflection helper cập-nhật lên 5 args cho existing unit test 4/4b/4c.
//
// ⚠️ OBSERVATION (REPORT em main, KHÔNG fix): isSystem KHÔNG reachable qua public
//   ApproveV2Async — branch APPROVE STEP (TransitionAsync line 243) gate
//   `decision == Approve`, trong khi isSystem cần `decision == AutoApprove`. PE KHÔNG có
//   SLA-auto-job gọi TransitionAsync (chỉ Contract SlaExpiryJob). Nên nhánh isSystem-
//   exempt trong PE là defensive/dead qua đường V2-approve. Vẫn test ĐƯỢC contract của nó
//   ở mức UNIT qua reflection (private static) — verify hành vi tài liệu hoá đúng. Phần
//   reachable (human approver) test qua integration TransitionAsync bên dưới.
//
// Mirror harness PeCcmThresholdFinalizeTests cùng folder.
public class PeApprovedPriceFinalizeTests
{
    private const string ValidSource = "Ncc";
    private const decimal ValidAmount = 750_000_000m;

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

    // Workflow V2 1 bước / 1 cấp = 1 approver (CEO/last). Approve cấp đó = terminal
    // DaDuyet qua normal advance (ApproveV2Async line 881 nextIdx>=steps.Count).
    private static async Task<ApprovalWorkflow> SeedSingleApproverWorkflowAsync(
        TestApplicationDbContext db, Guid approverId)
    {
        var wf = new ApprovalWorkflow
        {
            Code = "QT-AP-V2",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
            Name = "QT test approved-price",
            IsActive = true,
            IsUserSelectable = true,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflowId = wf.Id,
            Order = 1,
            Name = "Bước 1",
        };
        step.Levels.Add(new ApprovalWorkflowLevel
        {
            ApprovalWorkflowStepId = step.Id,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approverId,
        });
        wf.Steps.Add(step);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync(CancellationToken.None);
        return wf;
    }

    private static PurchaseEvaluation BuildPeAtLastSlot(Guid awId, string code)
        => new()
        {
            Id = Guid.NewGuid(),
            Type = PurchaseEvaluationType.DuyetNcc,
            Phase = PurchaseEvaluationPhase.ChoDuyet,
            MaPhieu = code,
            TenGoiThau = "Test giá chốt",
            ProjectId = Guid.NewGuid(),
            DrafterUserId = Guid.NewGuid(),
            ApprovalWorkflowId = awId,
            CurrentWorkflowStepIndex = 0,
            CurrentApprovalLevelOrder = 1,
            SlaDeadline = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc),
        };

    private static Task ApproveAsync(
        PurchaseEvaluationWorkflowService svc, PurchaseEvaluation pe, Guid actorUserId,
        string[] roles, decimal? amount, string? source,
        decimal? maxAmount = null) =>
        svc.TransitionAsync(
            evaluation: pe,
            targetPhase: PurchaseEvaluationPhase.ChoDuyet,
            actorUserId: actorUserId,
            actorRoles: roles,
            decision: ApprovalDecision.Approve,
            comment: null,
            approvedPriceAmount: amount,
            approvedPriceSource: source,
            approvedPriceMaxAmount: maxAmount,   // [S117] duyệt theo KHOẢNG — SAU source, mirror prod signature
            ct: CancellationToken.None);

    // =====================================================================
    // 1. Duyệt cấp cuối (terminal DaDuyet) HUMAN truyền giá hợp lệ → set đúng
    //    evaluation.ApprovedPriceAmount/Source. (reachable integration path)
    // =====================================================================
    [Fact]
    public async Task ApproveV2_TerminalApprove_ValidPrice_SetsApprovedPriceAndSource()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP1@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-001");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ValidAmount, source: ValidSource);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "1 bước/1 cấp duyệt → terminal");
            pe.ApprovedPriceAmount.Should().Be(ValidAmount, "giá chốt người duyệt chọn được bind");
            pe.ApprovedPriceSource.Should().Be(ValidSource);
        }
    }

    // =====================================================================
    // 2. Duyệt cấp cuối HUMAN KHÔNG truyền giá (null) → ConflictException
    //    ("Chọn 1 giá chốt..."). Phiếu KHÔNG đổi sang DaDuyet (throw trước set Phase).
    // =====================================================================
    [Fact]
    public async Task ApproveV2_TerminalApprove_NullPrice_Human_ThrowsConflict_NotFinalized()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP2@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-002");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: null, source: null);

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

            // ApplyApprovedPriceOnFinalize chạy TRƯỚC set Phase=DaDuyet (line 885-886)
            // → throw = phiếu giữ ChoDuyet, pointer chưa null.
            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "thiếu giá chốt → không finalize");
            reloaded.ApprovedPriceAmount.Should().BeNull();
        }
    }

    // =====================================================================
    // 3. Duyệt cấp cuối HUMAN truyền nguồn RÁC (∉ {Ncc,ProMin,ProMax,Ccm}) →
    //    ConflictException. Amount có nhưng source invalid → vẫn chặn.
    // =====================================================================
    [Fact]
    public async Task ApproveV2_TerminalApprove_GarbageSource_ThrowsConflict()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP3@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-003");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ValidAmount, source: "TuNghi");   // rác

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

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet);
            reloaded.ApprovedPriceSource.Should().BeNull();
        }
    }

    // =====================================================================
    // 3b. Mỗi nguồn hợp lệ (Ncc/ProMin/ProMax/Ccm) đều set được — chốt allow-list
    //     đầy đủ (parametrized theo Theory).
    // =====================================================================
    [Theory]
    [InlineData("Ncc")]
    [InlineData("ProMin")]
    [InlineData("ProMax")]
    [InlineData("Ccm")]
    public async Task ApproveV2_TerminalApprove_EachValidSource_SetsSource(string source)
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync($"ceoAP3b-{source}@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, $"PE-AP-3B-{source}");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ValidAmount, source: source);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet);
            pe.ApprovedPriceSource.Should().Be(source);
            pe.ApprovedPriceAmount.Should().Be(ValidAmount);
            // [S117 regression] nguồn ĐƠN → MaxAmount PHẢI null (bất-biến Max ⟺ ProMinMax).
            // Chứng signature mới (thêm maxAmount) KHÔNG phá path cũ.
            pe.ApprovedPriceMaxAmount.Should().BeNull("nguồn đơn không kèm giá Max");
        }
    }

    // =====================================================================
    // [S117] "Duyệt theo KHOẢNG" (source="ProMinMax") — reachable terminal path (mirror case 1).
    // Amount = giá Min, ApprovedPriceMaxAmount = giá Max. Financial-critical → test-before merge.
    // =====================================================================
    private const decimal ProMinMaxSource_MinPrice = 100_000_000m;   // giá Min PRO
    private const decimal ProMinMaxSource_MaxPrice = 130_000_000m;   // giá Max PRO
    private const string ProMinMax = "ProMinMax";

    // (a) HAPPY: source="ProMinMax", amount=Min(100), maxAmount=Max(130) → terminal set CẢ HAI.
    [Fact]
    public async Task ApproveV2_TerminalApprove_ProMinMax_ValidRange_SetsMinAndMax()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP-MM1@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-MM1");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ProMinMaxSource_MinPrice, source: ProMinMax, maxAmount: ProMinMaxSource_MaxPrice);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "1 bước/1 cấp duyệt → terminal");
            pe.ApprovedPriceAmount.Should().Be(ProMinMaxSource_MinPrice, "duyệt theo khoảng: Amount = giá Min");
            pe.ApprovedPriceMaxAmount.Should().Be(ProMinMaxSource_MaxPrice, "duyệt theo khoảng: MaxAmount = giá Max");
            pe.ApprovedPriceSource.Should().Be(ProMinMax);
        }
    }

    // (a-boundary) Min == Max HỢP LỆ — guard prod là `amount > maxAmount` (strict). Đổi thành
    //             `>=` sẽ làm case này RED → lock ranh giới `>` vs `>=`.
    [Fact]
    public async Task ApproveV2_TerminalApprove_ProMinMax_MinEqualsMax_Ok()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP-MM2@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-MM2");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ProMinMaxSource_MaxPrice, source: ProMinMax, maxAmount: ProMinMaxSource_MaxPrice);

            pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet);
            pe.ApprovedPriceAmount.Should().Be(ProMinMaxSource_MaxPrice);
            pe.ApprovedPriceMaxAmount.Should().Be(ProMinMaxSource_MaxPrice, "min==max: khoảng suy biến 1 điểm vẫn set Max");
            pe.ApprovedPriceSource.Should().Be(ProMinMax);
        }
    }

    // (b) THIẾU Max (maxAmount=null) → ConflictException, phiếu KHÔNG finalize (giữ ChoDuyet, Max null).
    [Fact]
    public async Task ApproveV2_TerminalApprove_ProMinMax_MissingMax_ThrowsConflict_NotFinalized()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP-MM3@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-MM3");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ProMinMaxSource_MinPrice, source: ProMinMax, maxAmount: null);

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*phải có cả giá Min và giá Max*");

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "thiếu Max → không finalize");
            reloaded.ApprovedPriceMaxAmount.Should().BeNull();
            reloaded.ApprovedPriceAmount.Should().BeNull();
        }
    }

    // (c) Min > Max (amount=130, maxAmount=100) → ConflictException, KHÔNG finalize.
    [Fact]
    public async Task ApproveV2_TerminalApprove_ProMinMax_MinGreaterThanMax_ThrowsConflict_NotFinalized()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP-MM4@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-MM4");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ProMinMaxSource_MaxPrice, source: ProMinMax, maxAmount: ProMinMaxSource_MinPrice);  // 130 > 100

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*không được lớn hơn giá Max*");

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet);
            reloaded.ApprovedPriceMaxAmount.Should().BeNull();
        }
    }

    // (d) Nguồn ĐƠN (Ncc) kèm maxAmount → wire-bug guard: ConflictException, KHÔNG finalize. Giữ
    //     bất-biến Max ⟺ ProMinMax (FE gửi Max cho nguồn đơn = sai → chặn).
    [Fact]
    public async Task ApproveV2_TerminalApprove_SingleSourceWithMax_ThrowsConflict_NotFinalized()
    {
        var (svc, fix, db, _) = CreateService();
        using (fix)
        {
            var ceo = (await fix.CreateUserAsync("ceoAP-MM5@ap.test", "CEO User", null, new[] { AppRoles.Director })).Id;
            var wf = await SeedSingleApproverWorkflowAsync(db, ceo);

            var pe = BuildPeAtLastSlot(wf.Id, "PE-AP-MM5");
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync(CancellationToken.None);

            var act = async () => await ApproveAsync(svc, pe, ceo, new[] { AppRoles.Director },
                amount: ProMinMaxSource_MinPrice, source: "Ncc", maxAmount: ProMinMaxSource_MaxPrice);  // nguồn đơn + Max

            await act.Should().ThrowAsync<ConflictException>()
                .WithMessage("*kèm giá Max*");

            var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
            reloaded.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "nguồn đơn kèm Max = wire-bug → chặn");
            reloaded.ApprovedPriceMaxAmount.Should().BeNull();
            reloaded.ApprovedPriceAmount.Should().BeNull();
        }
    }

    // =====================================================================
    // 4. Auto-approve hệ thống (isSystem=true) tới terminal mà null giá → KHÔNG throw
    //    (miễn chọn giá). Test UNIT qua reflection private-static ApplyApprovedPriceOnFinalize
    //    — isSystem KHÔNG reachable qua public ApproveV2Async (xem header OBSERVATION),
    //    nên test contract của method trực tiếp. Verify exempt-branch hoạt động đúng.
    // =====================================================================
    [Fact]
    public void ApplyApprovedPriceOnFinalize_System_NullPrice_NoThrow_NoSet()
    {
        var pe = new PurchaseEvaluation { Id = Guid.NewGuid(), TenGoiThau = "x" };

        var act = () => InvokeApply(pe, isSystem: true, amount: null, source: null);

        act.Should().NotThrow("auto-approve hệ thống MIỄN chọn giá chốt");
        pe.ApprovedPriceAmount.Should().BeNull("không có người chọn → không set giá");
        pe.ApprovedPriceSource.Should().BeNull();
    }

    // =====================================================================
    // 4b. Đối chứng (mirror case 2 ở mức unit): isSystem=false + null giá → Conflict.
    //     Cùng method, chỉ khác cờ isSystem → chứng minh exempt-branch là do isSystem.
    // =====================================================================
    [Fact]
    public void ApplyApprovedPriceOnFinalize_Human_NullPrice_ThrowsConflict()
    {
        var pe = new PurchaseEvaluation { Id = Guid.NewGuid(), TenGoiThau = "x" };

        var act = () => InvokeApply(pe, isSystem: false, amount: null, source: null);

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

    // =====================================================================
    // 4c. isSystem=true NHƯNG có truyền amount + source rác → vẫn Conflict (exempt
    //     chỉ áp khi amount==null; có amount thì source PHẢI hợp lệ kể cả system).
    // =====================================================================
    [Fact]
    public void ApplyApprovedPriceOnFinalize_System_AmountWithGarbageSource_ThrowsConflict()
    {
        var pe = new PurchaseEvaluation { Id = Guid.NewGuid(), TenGoiThau = "x" };

        var act = () => InvokeApply(pe, isSystem: true, amount: ValidAmount, source: "Bogus");

        act.Should().Throw<ConflictException>().WithMessage("*Nguồn giá chốt*");
    }

    // Reflection helper — invoke private static ApplyApprovedPriceOnFinalize. Unwrap
    // TargetInvocationException để assertion bắt đúng inner ConflictException.
    private static void InvokeApply(
        PurchaseEvaluation evaluation, bool isSystem, decimal? amount, string? source,
        decimal? maxAmount = null)
    {
        var mi = typeof(PurchaseEvaluationWorkflowService).GetMethod(
            "ApplyApprovedPriceOnFinalize",
            BindingFlags.NonPublic | BindingFlags.Static)
            ?? throw new InvalidOperationException("ApplyApprovedPriceOnFinalize không tìm thấy (đổi tên?).");
        try
        {
            // [S117] prod method nay 5 params (thêm maxAmount) — Invoke cần đúng số arg (optional
            // KHÔNG tự bind qua reflection) nếu không TargetParameterCountException.
            mi.Invoke(null, new object?[] { evaluation, isSystem, amount, source, maxAmount });
        }
        catch (TargetInvocationException ex) when (ex.InnerException is not null)
        {
            throw ex.InnerException;
        }
    }
}
