using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.PurchaseEvaluations;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.PurchaseEvaluations;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Application;

// ===== NEW (Mig 59 2026-06-24 anh Kiệt FDC / CCM) — setter "NS kỳ này" cột CCM =====
// Section B (THỰC HIỆN) tách 3 cột Dự án|PRO|CCM. PRO dùng /budget-adjust sẵn có
// (BudgetPeriodAmount); CCM nhập "NS kỳ này" RIÊNG ở cột mình → PeSuggestedPriceFeatures.cs
// SetPeCcmBudgetPeriodCommand(+Validator+Handler). Test theo CODE đã land (S34 rule —
// KHÔNG touch production). Mirror harness PeSuggestedPriceSetterAuthzTests (handler 2-dep
// nhẹ db+ICurrentUser instantiate trực tiếp + FakeCurrentUser(params roles) + SeedPeAsync).
//
// Authz (mirror UpdatePeSuggestedPriceCcm / UpdatePeBudgetCcm S61):
//   - role CostControl (CCM) HOẶC Admin set CcmBudgetPeriodAmount; role khác →
//     ForbiddenException ("*CCM*") fail-closed TRƯỚC mọi side-effect (guard sau NotFound).
//   - Absolute-set (null = clear). Budget=0 hợp lệ (GreaterThanOrEqualTo(0).When(HasValue)).
//   - KHÔNG submit-guard (guard chỉ trên BudgetPeriodAmount của PRO — gotcha #73 không đụng).
//
// ⚠️ Handler check PE-existence (NotFound) TRƯỚC authz gate (PeSuggestedPriceFeatures
//   line 172-173 rồi 176-181). Nên unknown-PE → NotFound bất kể role; Forbidden cần PE tồn tại.
//   Changelog.UserId = Guid? KHÔNG FK User (chỉ FK PE Cascade) → FakeCurrentUser random Guid an toàn.
public class PeCcmBudgetPeriodSetterAuthzTests
{
    private sealed class FakeCurrentUser(params string[] roles) : ICurrentUser
    {
        public Guid? UserId { get; } = Guid.NewGuid();
        public string? Email { get; } = "actor@test.local";
        public string? FullName { get; } = "Actor Test";
        public IReadOnlyList<string> Roles { get; } = roles ?? Array.Empty<string>();
        public bool IsAuthenticated => UserId is not null;
    }

    private static async Task<PurchaseEvaluation> SeedPeAsync(
        TestApplicationDbContext db, string code = "PE-CCMB-001")
    {
        var pe = new PurchaseEvaluation
        {
            Id = Guid.NewGuid(),
            Type = PurchaseEvaluationType.DuyetNcc,
            Phase = PurchaseEvaluationPhase.ChoDuyet,
            MaPhieu = code,
            TenGoiThau = "Gói thầu test NS kỳ này (CCM)",
            ProjectId = Guid.NewGuid(),
            DrafterUserId = Guid.NewGuid(),
        };
        db.PurchaseEvaluations.Add(pe);
        await db.SaveChangesAsync(CancellationToken.None);
        return pe;
    }

    // 1a. Admin set được CcmBudgetPeriodAmount (allow-list thứ nhất).
    [Fact]
    public async Task Admin_SetsCcmBudgetPeriod()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var pe = await SeedPeAsync(db, "PE-CCMB-001A");
        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.Admin));

        await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(pe.Id, 250_000_000m), CancellationToken.None);

        var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
        reloaded.CcmBudgetPeriodAmount.Should().Be(250_000_000m, "Admin được nhập NS kỳ này (CCM)");
    }

    // 1b. CostControl (CCM) set được CcmBudgetPeriodAmount (allow-list thứ hai).
    [Fact]
    public async Task CostControl_SetsCcmBudgetPeriod()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var pe = await SeedPeAsync(db, "PE-CCMB-001B");
        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.CostControl));

        await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(pe.Id, 333_000_000m), CancellationToken.None);

        var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
        reloaded.CcmBudgetPeriodAmount.Should().Be(333_000_000m, "CCM (CostControl) được nhập NS kỳ này cột mình");
    }

    // 1c. Role khác (Drafter — phiếu của người khai, KHÔNG có vai CCM) → ForbiddenException
    //     + KHÔNG set giá (fail-closed: role-gate TRƯỚC side-effect → no partial-write).
    [Fact]
    public async Task OtherRole_Drafter_ThrowsForbidden_NoSet()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var pe = await SeedPeAsync(db, "PE-CCMB-001C");
        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.Drafter));

        var act = async () => await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(pe.Id, 1m), CancellationToken.None);

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

        var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
        reloaded.CcmBudgetPeriodAmount.Should().BeNull("Drafter (không phải CCM/Admin) bị chặn → không set giá");
    }

    // 1c-bis. Role khác (Procurement = PRO, có cột riêng /budget-adjust) → cũng Forbidden
    //         (chắc chắn không chỉ Drafter bị chặn — PRO KHÔNG được nhập cột CCM).
    [Fact]
    public async Task OtherRole_Procurement_ThrowsForbidden()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var pe = await SeedPeAsync(db, "PE-CCMB-001CP");
        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.Procurement));

        var act = async () => await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(pe.Id, 9m), CancellationToken.None);

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

        var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
        reloaded.CcmBudgetPeriodAmount.Should().BeNull("PRO bị chặn → không set cột CCM (cột riêng độc lập)");
    }

    // 1d. value = 0 hợp lệ (no throw) — budget=0 cho ccmBudgetPeriodAmount luôn được
    //     (validator GreaterThanOrEqualTo(0).When(HasValue)). Handler set 0 vào DB.
    [Fact]
    public async Task ZeroValue_IsValid_AndPersisted()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var pe = await SeedPeAsync(db, "PE-CCMB-001D");
        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.CostControl));

        await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(pe.Id, 0m), CancellationToken.None);

        var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
        reloaded.CcmBudgetPeriodAmount.Should().Be(0m, "budget=0 hợp lệ cho cột CCM (no submit-guard ở đây)");

        // Validator cũng xác nhận 0 valid (và âm invalid).
        var validator = new SetPeCcmBudgetPeriodCommandValidator();
        validator.Validate(new SetPeCcmBudgetPeriodCommand(pe.Id, 0m))
            .IsValid.Should().BeTrue("0 hợp lệ");
        validator.Validate(new SetPeCcmBudgetPeriodCommand(pe.Id, -1m))
            .IsValid.Should().BeFalse("giá âm vi phạm GreaterThanOrEqualTo(0)");
    }

    // 1e. Absolute-set null = clear — bắt đầu từ giá CÓ SẴN rồi gửi null → CLEAR
    //     (chứng minh absolute-set, KHÔNG skip-if-null giữ giá trị cũ — gotcha #73).
    [Fact]
    public async Task NullValue_ClearsExistingAmount_AbsoluteSet()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var pe = await SeedPeAsync(db, "PE-CCMB-001E");

        // Pre-seed giá có sẵn để chứng minh bị xoá (không phải vốn-dĩ-null).
        pe.CcmBudgetPeriodAmount = 500_000_000m;
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.CostControl));
        await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(pe.Id, null), CancellationToken.None);

        var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
        reloaded.CcmBudgetPeriodAmount.Should().BeNull("absolute-set: null CLEAR field, KHÔNG giữ giá cũ");

        // Validator: null hợp lệ (clear).
        new SetPeCcmBudgetPeriodCommandValidator()
            .Validate(new SetPeCcmBudgetPeriodCommand(pe.Id, null))
            .IsValid.Should().BeTrue("null (clear) hợp lệ");
    }

    // 2. Unknown PE → NotFound (existence check TRƯỚC authz gate — line 172-173 < 176).
    [Fact]
    public async Task UnknownPe_ThrowsNotFound()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        var handler = new SetPeCcmBudgetPeriodCommandHandler(db, new FakeCurrentUser(AppRoles.CostControl));

        var act = async () => await handler.Handle(
            new SetPeCcmBudgetPeriodCommand(Guid.NewGuid(), 1m), CancellationToken.None);

        await act.Should().ThrowAsync<NotFoundException>();
    }
}
