using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
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;

// Phase 11 P11-D (S53 2026-06-08 — Wave 2) — test-after CreateItTicketHandler
// round-robin auto-assign + SLA-due theo Priority.
//
// READ src: WorkflowAppsFeatures.cs CreateItTicketHandler:
//   - SlaWindow map (dòng ~336): Urgent=4h / High=8h / Medium=24h / Low=72h
//     → e.SlaDueAt = e.CreatedAt + window (default Medium 24h nếu Priority lạ).
//   - Round-robin (dòng ~371-388): tìm Department.Code=="IT" (&& !IsDeleted) →
//     trong dept đó pick user IsActive ÍT ticket-mở nhất (Status != Closed && != Resolved
//     && !IsDeleted) → OrderBy(count).ThenBy(u.Id) → least-loaded + tie-break Id.
//     Không có dept IT / 0 user active IT → AssignedToUserId == null (unassigned).
//
// ⚠️ Tie-break ThenBy(u.Id) so theo Guid — CreateUserAsync dùng Guid.NewGuid() nên
// KHÔNG hardcode được "A thắng B". Assert động: tính expected = user có Id nhỏ hơn
// (Guid CompareTo) tại runtime, so với AssignedToUserId thực tế.
//
// GOTCHA Serializable-on-SQLite = NON-ISSUE (confirmed S52 ItTicketCodeGenTests):
// codegen MaTicket dùng BeginTransactionAsync(IsolationLevel.Serializable) chạy SẠCH
// trên SQLite (provider map isolation gracefully, no throw). Mọi Create dưới đây gen
// MaTicket bình thường — Case regression (3) verify lại format không vỡ khi handler đổi.
public class ItTicketAssignSlaTests
{
    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);
    }

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

    private static CreateItTicketCommand BuildCmd(int priority = 2, string title = "Máy in hỏng")
        => new(title, "Máy in tầng 3 không nhận lệnh in.", Category: 1, Priority: priority);

    // Seed 1 Department với Code chỉ định, trả về Id.
    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 đã assign cho user (mô phỏng "load sẵn") — bỏ qua codegen.
    private static async Task SeedOpenTicketAsync(TestApplicationDbContext db, Guid assigneeId, DateTime createdAt)
    {
        db.ItTickets.Add(new ItTicket
        {
            Id = Guid.NewGuid(),
            MaTicket = "IT/2026/900",                 // mã seed thủ công, không đụng sequence
            RequesterUserId = assigneeId,
            RequesterFullName = "seed",
            Title = "Ticket seed",
            Description = "load sẵn",
            Category = ItTicketCategory.Hardware,
            Priority = ItTicketPriority.Medium,
            Status = ItTicketStatus.Open,
            AssignedToUserId = assigneeId,
            CreatedAt = createdAt,
        });
        await db.SaveChangesAsync(CancellationToken.None);
    }

    // ============ Case 1: Round-robin least-loaded + tie-break deterministic ============

    [Fact]
    public async Task CreateItTicket_RoundRobin_PicksLeastLoadedThenTieBreaksById()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var requester = await fix.CreateUserAsync("req-rr1@test.local", "Người tạo", null, Array.Empty<string>());

            // 2 IT staff active, A có sẵn 1 ticket mở → load A=1, B=0.
            var staffA = await fix.CreateUserAsync("it-a@test.local", "IT Staff A", itDeptId, Array.Empty<string>());
            var staffB = await fix.CreateUserAsync("it-b@test.local", "IT Staff B", itDeptId, Array.Empty<string>());
            await SeedOpenTicketAsync(db, staffA.Id, clock.UtcNow);

            var handler = new CreateItTicketHandler(db, AsUser(requester), clock);

            // Create #1 → B (load 0) thắng A (load 1).
            var id1 = await handler.Handle(BuildCmd(), CancellationToken.None);
            var t1 = await db.ItTickets.FirstAsync(t => t.Id == id1);
            t1.AssignedToUserId.Should().Be(staffB.Id, "B load=0 < A load=1 → least-loaded");
            t1.AssignedToFullName.Should().Be("IT Staff B", "denorm full name set cùng assignee");

            // Create #2 → giờ A=1, B=1 tie → ThenBy(u.Id) chọn user Id nhỏ hơn (deterministic).
            var id2 = await handler.Handle(BuildCmd(), CancellationToken.None);
            var t2 = await db.ItTickets.FirstAsync(t => t.Id == id2);

            var expectedTieWinner = staffA.Id.CompareTo(staffB.Id) < 0 ? staffA : staffB;
            t2.AssignedToUserId.Should().Be(expectedTieWinner.Id,
                "A=1,B=1 tie → ThenBy(u.Id) → user có Guid Id nhỏ hơn");
        }
    }

    // ============ Case 2a: Không có Department "IT" → unassigned ============

    [Fact]
    public async Task CreateItTicket_NoItDepartment_LeavesUnassigned()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            // Chỉ có phòng khác (HR), KHÔNG có Code=="IT".
            var hrDeptId = await SeedDeptAsync(db, "HR", "Phòng Nhân sự");
            var requester = await fix.CreateUserAsync("req-rr2@test.local", "Người tạo", null, Array.Empty<string>());
            await fix.CreateUserAsync("hr-1@test.local", "HR Staff", hrDeptId, Array.Empty<string>());

            var handler = new CreateItTicketHandler(db, AsUser(requester), clock);
            var id = await handler.Handle(BuildCmd(), CancellationToken.None);

            var t = await db.ItTickets.FirstAsync(t => t.Id == id);
            t.AssignedToUserId.Should().BeNull("không có Department.Code==\"IT\" → bỏ qua round-robin");
            t.AssignedToFullName.Should().BeNull();
        }
    }

    // ============ Case 2b: Có dept IT nhưng 0 user active trong đó → unassigned ============

    [Fact]
    public async Task CreateItTicket_ItDepartmentButNoActiveStaff_LeavesUnassigned()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var requester = await fix.CreateUserAsync("req-rr3@test.local", "Người tạo", null, Array.Empty<string>());
            // Không seed user nào thuộc IT dept.

            var handler = new CreateItTicketHandler(db, AsUser(requester), clock);
            var id = await handler.Handle(BuildCmd(), CancellationToken.None);

            var t = await db.ItTickets.FirstAsync(t => t.Id == id);
            t.AssignedToUserId.Should().BeNull("dept IT tồn tại nhưng 0 user → assignee null");
        }
    }

    // ============ Case 3: User ngoài IT / inactive KHÔNG được assign ============

    [Fact]
    public async Task CreateItTicket_OnlyAssignsActiveStaffInsideItDepartment()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            var otherDeptId = await SeedDeptAsync(db, "FIN", "Phòng Tài chính");
            var requester = await fix.CreateUserAsync("req-rr4@test.local", "Người tạo", null, Array.Empty<string>());

            // (a) user phòng khác — KHÔNG đủ điều kiện.
            await fix.CreateUserAsync("fin-1@test.local", "Finance Staff", otherDeptId, Array.Empty<string>());
            // (b) user IT nhưng IsActive=false — KHÔNG đủ điều kiện.
            var inactiveIt = await fix.CreateUserAsync("it-off@test.local", "IT Inactive", itDeptId, Array.Empty<string>());
            inactiveIt.IsActive = false;
            await db.SaveChangesAsync(CancellationToken.None);
            // (c) đúng 1 IT staff active — phải là người duy nhất được assign.
            var activeIt = await fix.CreateUserAsync("it-on@test.local", "IT Active", itDeptId, Array.Empty<string>());

            var handler = new CreateItTicketHandler(db, AsUser(requester), clock);
            var id = await handler.Handle(BuildCmd(), CancellationToken.None);

            var t = await db.ItTickets.FirstAsync(t => t.Id == id);
            t.AssignedToUserId.Should().Be(activeIt.Id,
                "chỉ user IsActive thuộc đúng dept IT mới vào pool round-robin");
        }
    }

    // ============ Case 4: SLA-due = CreatedAt + window theo Priority ============

    [Theory]
    [InlineData(4, 4)]    // Urgent → +4h
    [InlineData(3, 8)]    // High   → +8h
    [InlineData(2, 24)]   // Medium → +24h
    [InlineData(1, 72)]   // Low    → +72h
    public async Task CreateItTicket_SlaDueAt_MatchesPriorityWindow(int priority, int expectedHours)
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            var requester = await fix.CreateUserAsync($"req-sla{priority}@test.local", "Người tạo", null, Array.Empty<string>());

            var handler = new CreateItTicketHandler(db, AsUser(requester), clock);
            var id = await handler.Handle(BuildCmd(priority), CancellationToken.None);

            var t = await db.ItTickets.FirstAsync(t => t.Id == id);
            t.CreatedAt.Should().Be(FixedNow, "clock stub cố định → CreatedAt = clock.UtcNow");
            t.SlaDueAt.Should().Be(FixedNow.AddHours(expectedHours),
                $"Priority {priority} → SLA window {expectedHours}h từ CreatedAt");
            t.SlaBreached.Should().BeFalse("ticket mới chưa breach");
            t.SlaWarnedSent.Should().BeFalse("ticket mới chưa cảnh báo");
        }
    }

    // ============ Case 5: Regression P11-F — MaTicket vẫn gen đúng format ============

    [Fact]
    public async Task CreateItTicket_StillGeneratesMaTicket_FormatUnchanged()
    {
        var (fix, db, clock) = NewCtx();
        using (fix)
        {
            // Có dept IT + staff để chạy luôn nhánh round-robin → đảm bảo codegen
            // vẫn chạy chung với assign logic, không bị nhánh mới phá format.
            var itDeptId = await SeedDeptAsync(db, "IT", "Phòng CNTT");
            await fix.CreateUserAsync("it-reg@test.local", "IT Staff", itDeptId, Array.Empty<string>());
            var requester = await fix.CreateUserAsync("req-reg@test.local", "Người tạo", null, Array.Empty<string>());

            var handler = new CreateItTicketHandler(db, AsUser(requester), clock);
            var id = await handler.Handle(BuildCmd(), CancellationToken.None);

            var t = await db.ItTickets.FirstAsync(t => t.Id == id);
            t.MaTicket.Should().NotBeNullOrEmpty();
            t.MaTicket.Should().MatchRegex(@"^IT/\d{4}/\d{3}$", "P11-D đổi handler nhưng codegen P11-F giữ format");
            t.MaTicket.Should().Be("IT/2026/001", "vẫn seq đầu năm 2026");
        }
    }
}
