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

namespace SolutionErp.Infrastructure.Tests.Application;

// S54 (2026-06-08) — SECURITY GUARD test-before-merge cho ItTicket reassign authz.
//
// READ src: WorkflowAppsFeatures.cs REGION 5 (dòng ~443-522):
//   A. GetAssignableItStaffHandler(db, cu):
//        itDeptId = Departments.Where(Code=="IT" && !IsDeleted).
//        isAdmin = cu.Roles.Contains("Admin"); myDeptId = Users.Where(Id==cu.UserId).DepartmentId.
//        isItStaff = (myDeptId == itDeptId). canReassign = isAdmin || isItStaff.
//        !canReassign (hoặc không có dept IT) → (canReassign, EMPTY).
//        canReassign → Staff = Users.Where(DeptId==itDeptId && IsActive).OrderBy(FullName).Select(Id,FullName).
//        UserId null → UnauthorizedException.
//   B. AssignItTicketHandler(db, cu, clock) — guard MỚI S54:
//        caller phải Admin HOẶC myDeptId==itDeptId → else ForbiddenException.
//        assignee.DepartmentId phải == itDeptId → else ConflictException("Người được giao phải thuộc tổ IT.").
//        Giữ: UserId null→Unauthorized; ticket !found→NotFound; assignee !found(IsActive)→NotFound("User",id).
//
// Pattern reuse (mirror ItTicketAssignSlaTests S53): IdentityFixture +
// TestApplicationDbContext + FixedDateTime + SeedDeptAsync. Fake ICurrentUser =
// TestCurrentUser với Roles configurable (matrix Admin / IT-staff / non-IT).
//
// Mục tiêu authz: case 5 (non-IT non-admin → assign) PHẢI throw Forbidden — đây là
// case RED nếu guard chưa có. Empty-staff = 0-leak assert (Staff.Should().BeEmpty).
public class ItTicketReassignAuthzTests
{
    private static readonly DateTime FixedNow = new(2026, 6, 8, 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);
    }

    // Fake ICurrentUser từ 1 User seed + roles tùy chọn (matrix Admin / [] non-admin).
    private static TestCurrentUser AsUser(User u, params string[] roles)
        => new() { UserId = u.Id, FullName = u.FullName, Roles = roles ?? Array.Empty<string>() };

    private static async Task<Guid> SeedDeptAsync(TestApplicationDbContext db, string code, string name)
    {
        var dept = new Department { Id = Guid.NewGuid(), Code = code, Name = name };
        db.Departments.Add(dept);
        await db.SaveChangesAsync(CancellationToken.None);
        return dept.Id;
    }

    // Seed 1 ticket OPEN unassigned để reassign — bỏ qua codegen (mã thủ công).
    private static async Task<Guid> SeedTicketAsync(TestApplicationDbContext db, Guid requesterId)
    {
        var t = new ItTicket
        {
            Id = Guid.NewGuid(),
            MaTicket = "IT/2026/901",
            RequesterUserId = requesterId,
            RequesterFullName = "seed-req",
            Title = "Ticket cần gán",
            Description = "reassign test",
            Category = ItTicketCategory.Hardware,
            Priority = ItTicketPriority.Medium,
            Status = ItTicketStatus.Open,
            CreatedAt = FixedNow,
        };
        db.ItTickets.Add(t);
        await db.SaveChangesAsync(CancellationToken.None);
        return t.Id;
    }

    // =====================================================================
    // GetAssignableItStaff — capability + staff scoping
    // =====================================================================

    // Case 1: Admin (dept null, KHÔNG ở IT) → CanReassign=true + Staff = đúng IT-active,
    // ordered theo FullName, KHÔNG lẫn dept khác / inactive.
    [Fact]
    public async Task GetAssignableItStaff_AdminCaller_ReturnsCanReassignAndOrderedItStaff()
    {
        var (fix, db, _) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var ktDeptId = await SeedDeptAsync(db, "KT", "Phòng Kế toán");

            // Admin dept null (chứng minh canReassign do role, không do dept IT).
            var admin = await fix.CreateUserAsync("admin@test.local", "Quản trị", null, new[] { "Admin" });
            // IT active — "Cao" và "Truong" (verify OrderBy FullName: Cao < Truong).
            await fix.CreateUserAsync("it-truong@test.local", "Truong", itDeptId, Array.Empty<string>());
            await fix.CreateUserAsync("it-cao@test.local", "Cao", itDeptId, Array.Empty<string>());
            // Noise: KT user + IT inactive — KHÔNG được xuất hiện.
            await fix.CreateUserAsync("kt-1@test.local", "Ke Toan", ktDeptId, Array.Empty<string>());
            var inactiveIt = await fix.CreateUserAsync("it-off@test.local", "IT Nghi", itDeptId, Array.Empty<string>());
            inactiveIt.IsActive = false;
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new GetAssignableItStaffHandler(db, AsUser(admin, "Admin"));
            var res = await handler.Handle(new GetAssignableItStaffQuery(), CancellationToken.None);

            res.CanReassign.Should().BeTrue("Admin luôn được reassign bất kể dept");
            res.Staff.Should().HaveCount(2, "chỉ 2 IT active — KT user + IT inactive bị loại");
            res.Staff.Select(s => s.FullName).Should().ContainInOrder("Cao", "Truong");
            res.Staff.Select(s => s.FullName).Should().NotContain("Ke Toan", "user dept khác không leak");
            res.Staff.Select(s => s.FullName).Should().NotContain("IT Nghi", "IT inactive không leak");
        }
    }

    // Case 2: IT staff (DeptId==IT, KHÔNG role Admin) → CanReassign=true + Staff = IT members.
    [Fact]
    public async Task GetAssignableItStaff_ItStaffCaller_ReturnsCanReassignAndItMembers()
    {
        var (fix, db, _) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");

            var caller = await fix.CreateUserAsync("it-self@test.local", "An", itDeptId, Array.Empty<string>());
            await fix.CreateUserAsync("it-binh@test.local", "Binh", itDeptId, Array.Empty<string>());

            // Caller KHÔNG có role Admin (Roles rỗng) → canReassign do isItStaff.
            var handler = new GetAssignableItStaffHandler(db, AsUser(caller));
            var res = await handler.Handle(new GetAssignableItStaffQuery(), CancellationToken.None);

            res.CanReassign.Should().BeTrue("thành viên tổ IT được reassign dù không phải Admin");
            res.Staff.Select(s => s.FullName).Should().ContainInOrder("An", "Binh");
            res.Staff.Should().HaveCount(2);
        }
    }

    // Case 3: non-IT non-admin (dept KT) → CanReassign=false + Staff EMPTY (0-leak).
    [Fact]
    public async Task GetAssignableItStaff_NonItNonAdmin_ReturnsCannotAndEmptyStaff()
    {
        var (fix, db, _) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var ktDeptId = await SeedDeptAsync(db, "KT", "Phòng Kế toán");

            // Dù có IT staff tồn tại, caller non-IT non-admin KHÔNG được thấy.
            await fix.CreateUserAsync("it-x@test.local", "IT X", itDeptId, Array.Empty<string>());
            var caller = await fix.CreateUserAsync("kt-caller@test.local", "Ke Toan Vien", ktDeptId, Array.Empty<string>());

            var handler = new GetAssignableItStaffHandler(db, AsUser(caller));
            var res = await handler.Handle(new GetAssignableItStaffQuery(), CancellationToken.None);

            res.CanReassign.Should().BeFalse("không Admin + không ở tổ IT → cấm reassign");
            res.Staff.Should().BeEmpty("không leak danh sách IT staff cho người không có quyền");
        }
    }

    // Case 3b: caller dept null + non-admin → cũng false + empty (myDeptId==null != itDeptId).
    [Fact]
    public async Task GetAssignableItStaff_NoDeptNonAdmin_ReturnsCannotAndEmptyStaff()
    {
        var (fix, db, _) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            await fix.CreateUserAsync("it-y@test.local", "IT Y", itDeptId, Array.Empty<string>());
            var caller = await fix.CreateUserAsync("nodept@test.local", "Khong Phong", null, Array.Empty<string>());

            var handler = new GetAssignableItStaffHandler(db, AsUser(caller));
            var res = await handler.Handle(new GetAssignableItStaffQuery(), CancellationToken.None);

            res.CanReassign.Should().BeFalse("dept null + non-admin → myDeptId != itDeptId");
            res.Staff.Should().BeEmpty();
        }
    }

    // Case 4 (optional): IT staff caller nhưng dept IT có user inactive → inactive bị loại.
    [Fact]
    public async Task GetAssignableItStaff_ExcludesInactiveItMembers()
    {
        var (fix, db, _) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var caller = await fix.CreateUserAsync("it-lead@test.local", "Active Lead", itDeptId, Array.Empty<string>());
            var inactive = await fix.CreateUserAsync("it-dead@test.local", "Inactive Mem", itDeptId, Array.Empty<string>());
            inactive.IsActive = false;
            await db.SaveChangesAsync(CancellationToken.None);

            var handler = new GetAssignableItStaffHandler(db, AsUser(caller));
            var res = await handler.Handle(new GetAssignableItStaffQuery(), CancellationToken.None);

            res.CanReassign.Should().BeTrue();
            res.Staff.Should().ContainSingle("chỉ caller active còn lại sau khi loại inactive");
            res.Staff.Single().FullName.Should().Be("Active Lead");
        }
    }

    // Case (guard): UserId null → UnauthorizedException.
    [Fact]
    public async Task GetAssignableItStaff_NoUserId_ThrowsUnauthorized()
    {
        var (fix, db, _) = NewCtx();
        using (fix)
        {
            var handler = new GetAssignableItStaffHandler(db, new TestCurrentUser());
            await FluentActions.Awaiting(() => handler.Handle(new GetAssignableItStaffQuery(), CancellationToken.None))
                .Should().ThrowAsync<UnauthorizedException>();
        }
    }

    // =====================================================================
    // AssignItTicket — authz guard + assignee constraint
    // =====================================================================

    // Case 5: non-IT non-admin caller → assign → ForbiddenException (case RED nếu thiếu guard).
    [Fact]
    public async Task AssignItTicket_NonItNonAdminCaller_ThrowsForbidden()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var ktDeptId = await SeedDeptAsync(db, "KT", "Phòng Kế toán");

            var assignee = await fix.CreateUserAsync("it-assignee@test.local", "IT Assignee", itDeptId, Array.Empty<string>());
            var caller = await fix.CreateUserAsync("kt-bad@test.local", "Ke Toan", ktDeptId, Array.Empty<string>());
            var ticketId = await SeedTicketAsync(db, caller.Id);

            var handler = new AssignItTicketHandler(db, AsUser(caller), clock);
            var cmd = new AssignItTicketCommand(ticketId, assignee.Id);

            await FluentActions.Awaiting(() => handler.Handle(cmd, CancellationToken.None))
                .Should().ThrowAsync<ForbiddenException>("caller không Admin + không ở tổ IT bị chặn reassign");

            // Side-effect guard: assign KHÔNG được áp dụng khi bị Forbidden.
            var t = await db.ItTickets.AsNoTracking().FirstAsync(x => x.Id == ticketId);
            t.AssignedToUserId.Should().BeNull("Forbidden → không mutate AssignedToUserId");
        }
    }

    // Case 5b (S56 #5): non-IT non-admin caller + ticketId KHÔNG tồn tại → vẫn Forbidden
    // (KHÔNG NotFound). Chứng minh authz chạy TRƯỚC ticket lookup → fail-closed, không rò rỉ
    // existence-oracle (NotFound vs Forbidden sẽ tiết lộ ticket có thật hay không).
    [Fact]
    public async Task AssignItTicket_NonItNonAdmin_NonexistentTicket_ThrowsForbiddenNotNotFound()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var ktDeptId = await SeedDeptAsync(db, "KT", "Phòng Kế toán");

            var caller = await fix.CreateUserAsync("kt-probe@test.local", "Ke Toan", ktDeptId, Array.Empty<string>());
            // KHÔNG seed ticket — ticketId hoàn toàn ngẫu nhiên (không tồn tại).
            var handler = new AssignItTicketHandler(db, AsUser(caller), clock);
            var cmd = new AssignItTicketCommand(Guid.NewGuid(), Guid.NewGuid());

            // Phải Forbidden, KHÔNG được NotFound (nếu NotFound → leak: caller suy ra ticket không có).
            await FluentActions.Awaiting(() => handler.Handle(cmd, CancellationToken.None))
                .Should().ThrowAsync<ForbiddenException>(
                    "authz fail-closed chạy trước lookup — non-IT nhận Forbidden cho MỌI ticketId");
        }
    }

    // Case 5c (S56 #5): cùng caller non-IT, lần này ticket CÓ tồn tại → cũng Forbidden.
    // Cặp 5b/5c chứng minh phản hồi GIỐNG NHAU (Forbidden) bất kể ticket tồn tại — không oracle.
    [Fact]
    public async Task AssignItTicket_NonItNonAdmin_ExistentTicket_AlsoThrowsForbidden_SameAsNonexistent()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var ktDeptId = await SeedDeptAsync(db, "KT", "Phòng Kế toán");

            var assignee = await fix.CreateUserAsync("it-ok@test.local", "IT Ok", itDeptId, Array.Empty<string>());
            var caller = await fix.CreateUserAsync("kt-probe2@test.local", "Ke Toan", ktDeptId, Array.Empty<string>());
            var ticketId = await SeedTicketAsync(db, caller.Id);   // ticket THẬT tồn tại

            var handler = new AssignItTicketHandler(db, AsUser(caller), clock);
            await FluentActions.Awaiting(() => handler.Handle(new AssignItTicketCommand(ticketId, assignee.Id), CancellationToken.None))
                .Should().ThrowAsync<ForbiddenException>("ticket tồn tại nhưng caller non-IT vẫn Forbidden — phản hồi không phân biệt");

            var t = await db.ItTickets.AsNoTracking().FirstAsync(x => x.Id == ticketId);
            t.AssignedToUserId.Should().BeNull("Forbidden → không mutate assignment");
        }
    }

    // Case 6: Admin caller, assignee ∈ IT → success (set AssignedTo* đúng).
    [Fact]
    public async Task AssignItTicket_AdminCaller_AssigneeInIt_Succeeds()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");

            var admin = await fix.CreateUserAsync("admin2@test.local", "Quản trị", null, new[] { "Admin" });
            var assignee = await fix.CreateUserAsync("it-target@test.local", "IT Target", itDeptId, Array.Empty<string>());
            var ticketId = await SeedTicketAsync(db, admin.Id);

            var handler = new AssignItTicketHandler(db, AsUser(admin, "Admin"), clock);
            await handler.Handle(new AssignItTicketCommand(ticketId, assignee.Id), CancellationToken.None);

            var t = await db.ItTickets.AsNoTracking().FirstAsync(x => x.Id == ticketId);
            t.AssignedToUserId.Should().Be(assignee.Id);
            t.AssignedToFullName.Should().Be("IT Target", "denorm full name set từ User.FullName");
            t.UpdatedBy.Should().Be(admin.Id);
        }
    }

    // Case 7: IT staff caller, assignee ∈ IT → success.
    [Fact]
    public async Task AssignItTicket_ItStaffCaller_AssigneeInIt_Succeeds()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");

            var caller = await fix.CreateUserAsync("it-caller@test.local", "IT Caller", itDeptId, Array.Empty<string>());
            var assignee = await fix.CreateUserAsync("it-peer@test.local", "IT Peer", itDeptId, Array.Empty<string>());
            var ticketId = await SeedTicketAsync(db, caller.Id);

            var handler = new AssignItTicketHandler(db, AsUser(caller), clock);
            await handler.Handle(new AssignItTicketCommand(ticketId, assignee.Id), CancellationToken.None);

            var t = await db.ItTickets.AsNoTracking().FirstAsync(x => x.Id == ticketId);
            t.AssignedToUserId.Should().Be(assignee.Id, "IT staff được reassign cho IT peer");
            t.AssignedToFullName.Should().Be("IT Peer");
        }
    }

    // Case 8: Admin caller, assignee KHÔNG thuộc IT → ConflictException.
    [Fact]
    public async Task AssignItTicket_AssigneeOutsideIt_ThrowsConflict()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var ktDeptId = await SeedDeptAsync(db, "KT", "Phòng Kế toán");

            var admin = await fix.CreateUserAsync("admin3@test.local", "Quản trị", null, new[] { "Admin" });
            // Assignee thuộc KT, KHÔNG phải IT → vi phạm constraint.
            var assignee = await fix.CreateUserAsync("kt-target@test.local", "KT Target", ktDeptId, Array.Empty<string>());
            var ticketId = await SeedTicketAsync(db, admin.Id);

            var handler = new AssignItTicketHandler(db, AsUser(admin, "Admin"), clock);
            await FluentActions.Awaiting(() => handler.Handle(new AssignItTicketCommand(ticketId, assignee.Id), CancellationToken.None))
                .Should().ThrowAsync<ConflictException>()
                .WithMessage("Người được giao phải thuộc tổ IT.");

            var t = await db.ItTickets.AsNoTracking().FirstAsync(x => x.Id == ticketId);
            t.AssignedToUserId.Should().BeNull("Conflict → không mutate assignment");
        }
    }

    // Case 9 (optional): assignee inactive → NotFoundException("User",...).
    [Fact]
    public async Task AssignItTicket_AssigneeInactive_ThrowsNotFound()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var admin = await fix.CreateUserAsync("admin4@test.local", "Quản trị", null, new[] { "Admin" });
            var inactive = await fix.CreateUserAsync("it-inact@test.local", "IT Inactive", itDeptId, Array.Empty<string>());
            inactive.IsActive = false;
            await db.SaveChangesAsync(CancellationToken.None);
            var ticketId = await SeedTicketAsync(db, admin.Id);

            var handler = new AssignItTicketHandler(db, AsUser(admin, "Admin"), clock);
            await FluentActions.Awaiting(() => handler.Handle(new AssignItTicketCommand(ticketId, inactive.Id), CancellationToken.None))
                .Should().ThrowAsync<NotFoundException>("assignee phải IsActive — inactive coi như not found");
        }
    }

    // Case (guard): ticket không tồn tại → NotFoundException("ItTicket",...). Authz pass trước.
    [Fact]
    public async Task AssignItTicket_TicketNotFound_ThrowsNotFound()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var admin = await fix.CreateUserAsync("admin5@test.local", "Quản trị", null, new[] { "Admin" });

            var handler = new AssignItTicketHandler(db, AsUser(admin, "Admin"), clock);
            await FluentActions.Awaiting(() =>
                    handler.Handle(new AssignItTicketCommand(Guid.NewGuid(), admin.Id), CancellationToken.None))
                .Should().ThrowAsync<NotFoundException>();
        }
    }

    // Case (guard): UserId null → UnauthorizedException (trước cả ticket lookup).
    [Fact]
    public async Task AssignItTicket_NoUserId_ThrowsUnauthorized()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var handler = new AssignItTicketHandler(db, new TestCurrentUser(), clock);
            await FluentActions.Awaiting(() =>
                    handler.Handle(new AssignItTicketCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None))
                .Should().ThrowAsync<UnauthorizedException>();
        }
    }
}
