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

namespace SolutionErp.Infrastructure.Tests.Application;

// S66 test-after (UAT mode — Department.ParentId + cây tổ chức shipped S65 Mig 51).
// File `DepartmentFeatures.cs`:
//   - GetDepartmentTreeQueryHandler: ráp cây từ ParentId loose-Guid + rollup
//     TotalEmployeeCount = Direct + Σ con đệ quy (nguồn = User.DepartmentId, active).
//   - UpdateDepartmentCommandHandler (line 198-213): cycle-guard 2 lớp khi gán ParentId:
//       (1) ParentId == Id  → ConflictException "Phòng không thể là cấp cha của chính nó."
//       (2) ParentId là con-cháu (đi ngược chuỗi cha, gặp lại Id) → ConflictException
//           "Không thể gán: sẽ tạo vòng lặp phân cấp phòng ban."
//
// CHỐT theo CODE (single source of truth, S34 rule):
//   - DirectEmployeeCount = User.DepartmentId == dept.Id AND IsActive=true.
//     EmployeeProfile KHÔNG có DepartmentId — org-chart đếm trên User entity (Mig 11).
//   - Children sort theo Code (StringComparer.Ordinal).
//   - Root = ParentId null HOẶC trỏ phòng không tồn tại (orphan-safe coi như root).
//   - Cycle-guard RollupAndSort dùng HashSet visited → data cũ đã lỗi cha-trỏ-vòng vẫn
//     không treo (cắt vòng, node đã thăm trả null).
//
// Vì handler đếm User.DepartmentId → seed User thật qua IdentityFixture (Identity stack).
// Cycle-guard test KHÔNG cần User → SqliteDbFixture đủ nhẹ.
public class DepartmentTreeTests
{
    // ---- Seed helper: tạo Department row, trả entity để lấy Id ----
    private static Department Dept(string code, string name, Guid? parentId = null)
        => new()
        {
            Id = Guid.NewGuid(),
            Code = code,
            Name = name,
            ParentId = parentId,
        };

    // ============================================================
    // GetDepartmentTree — ráp cây + rollup TotalEmployeeCount
    // ============================================================

    [Fact]
    public async Task Tree_NestedHierarchy_ParentContainsChildren_SortedByCode()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();

        // Cây:  ROOT
        //         ├─ A
        //         │   └─ A1
        //         └─ B
        var root = Dept("ROOT", "Công ty");
        var a = Dept("A", "Phòng A", root.Id);
        var a1 = Dept("A1", "Tổ A1", a.Id);
        var b = Dept("B", "Phòng B", root.Id);
        // Add lệch thứ tự Code để chứng minh sort ổn định (B trước A trong insert).
        db.Departments.AddRange(b, a1, root, a);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new GetDepartmentTreeQueryHandler(db);
        var tree = await handler.Handle(new GetDepartmentTreeQuery(), CancellationToken.None);

        // 1 root duy nhất.
        tree.Should().HaveCount(1);
        var rootNode = tree[0];
        rootNode.Id.Should().Be(root.Id);
        rootNode.Code.Should().Be("ROOT");

        // Children của ROOT sort theo Code: A trước B.
        rootNode.Children.Select(c => c.Code).Should().Equal("A", "B");

        // A chứa A1 (cha chứa con đúng phân cấp).
        var aNode = rootNode.Children.First(c => c.Code == "A");
        aNode.Children.Should().ContainSingle().Which.Code.Should().Be("A1");
        aNode.Children[0].ParentId.Should().Be(a.Id);

        // B là lá (không con).
        rootNode.Children.First(c => c.Code == "B").Children.Should().BeEmpty();
    }

    [Fact]
    public async Task Tree_RollupTotalEmployeeCount_DirectPlusAllDescendants()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();

        // Cây:  ROOT (2 NV trực tiếp)
        //         └─ A (3 NV)
        //             └─ A1 (1 NV)
        // Total: A1=1, A=3+1=4, ROOT=2+4=6.
        var root = Dept("ROOT", "Công ty");
        var a = Dept("A", "Phòng A", root.Id);
        var a1 = Dept("A1", "Tổ A1", a.Id);
        db.Departments.AddRange(root, a, a1);
        await db.SaveChangesAsync(CancellationToken.None);

        // Seed User active per dept (đếm theo User.DepartmentId + IsActive).
        await SeedUsersAsync(fix, root.Id, 2);
        await SeedUsersAsync(fix, a.Id, 3);
        await SeedUsersAsync(fix, a1.Id, 1);

        var handler = new GetDepartmentTreeQueryHandler(db);
        var tree = await handler.Handle(new GetDepartmentTreeQuery(), CancellationToken.None);

        var rootNode = tree.Single();
        var aNode = rootNode.Children.Single(c => c.Code == "A");
        var a1Node = aNode.Children.Single(c => c.Code == "A1");

        // Direct = NV trực thuộc.
        rootNode.DirectEmployeeCount.Should().Be(2);
        aNode.DirectEmployeeCount.Should().Be(3);
        a1Node.DirectEmployeeCount.Should().Be(1);

        // Total = Direct + Σ con đệ quy (rollup).
        a1Node.TotalEmployeeCount.Should().Be(1);
        aNode.TotalEmployeeCount.Should().Be(4);
        rootNode.TotalEmployeeCount.Should().Be(6);
    }

    [Fact]
    public async Task Tree_InactiveUser_NotCountedInDirectOrTotal()
    {
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();

        var root = Dept("ROOT", "Công ty");
        db.Departments.Add(root);
        await db.SaveChangesAsync(CancellationToken.None);

        // 2 active + 1 inactive trong cùng phòng → chỉ đếm 2 active.
        await SeedUsersAsync(fix, root.Id, 2);
        await SeedUsersAsync(fix, root.Id, 1, isActive: false);

        var handler = new GetDepartmentTreeQueryHandler(db);
        var tree = await handler.Handle(new GetDepartmentTreeQuery(), CancellationToken.None);

        var rootNode = tree.Single();
        rootNode.DirectEmployeeCount.Should().Be(2);
        rootNode.TotalEmployeeCount.Should().Be(2);
    }

    [Fact]
    public async Task Tree_OrphanParentId_TreatedAsRoot()
    {
        // ParentId trỏ tới Guid không tồn tại (data cũ / phòng cha đã xoá) →
        // node coi như root (orphan-safe) thay vì biến mất.
        using var fix = new IdentityFixture();
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();

        var ghostParentId = Guid.NewGuid();   // không seed vào DB
        var orphan = Dept("ORPH", "Phòng mồ côi", ghostParentId);
        db.Departments.Add(orphan);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new GetDepartmentTreeQueryHandler(db);
        var tree = await handler.Handle(new GetDepartmentTreeQuery(), CancellationToken.None);

        tree.Should().ContainSingle().Which.Code.Should().Be("ORPH");
    }

    // ============================================================
    // Update cycle-guard — invariant quan trọng nhất
    // ============================================================

    [Fact]
    public async Task Update_SetParentToSelf_ThrowsConflict()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var d = Dept("X", "Phòng X");
        db.Departments.Add(d);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new UpdateDepartmentCommandHandler(db);
        var cmd = new UpdateDepartmentCommand(d.Id, "X", "Phòng X", null, null, ParentId: d.Id);

        var act = async () => await handler.Handle(cmd, CancellationToken.None);

        await act.Should().ThrowAsync<ConflictException>()
            .WithMessage("*không thể là cấp cha của chính nó*");

        // Side-effect: ParentId KHÔNG bị mutate (vẫn null sau khi reject).
        var reload = await db.Departments.AsNoTracking().FirstAsync(x => x.Id == d.Id);
        reload.ParentId.Should().BeNull();
    }

    [Fact]
    public async Task Update_SetParentToDirectChild_CreatesCycle_ThrowsConflict()
    {
        // A là cha của B. Gán ParentId(A) = B → vòng A→B→A. Phải reject.
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var a = Dept("A", "Phòng A");
        var b = Dept("B", "Phòng B", a.Id);   // B con của A
        db.Departments.AddRange(a, b);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new UpdateDepartmentCommandHandler(db);
        // Update A: gán cha = B (con của A) → tạo vòng.
        var cmd = new UpdateDepartmentCommand(a.Id, "A", "Phòng A", null, null, ParentId: b.Id);

        var act = async () => await handler.Handle(cmd, CancellationToken.None);

        await act.Should().ThrowAsync<ConflictException>()
            .WithMessage("*sẽ tạo vòng lặp phân cấp*");

        // A.ParentId vẫn null (không mutate khi reject).
        var reloadA = await db.Departments.AsNoTracking().FirstAsync(x => x.Id == a.Id);
        reloadA.ParentId.Should().BeNull();
    }

    [Fact]
    public async Task Update_SetParentToDeepDescendant_CreatesCycle_ThrowsConflict()
    {
        // Chuỗi sâu: A → B → C (A là tổ tiên của C). Gán ParentId(A) = C →
        // vòng A→…→C→A. Guard đi ngược chuỗi cha từ C gặp lại A → reject.
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var a = Dept("A", "Phòng A");
        var b = Dept("B", "Phòng B", a.Id);
        var c = Dept("C", "Tổ C", b.Id);
        db.Departments.AddRange(a, b, c);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new UpdateDepartmentCommandHandler(db);
        var cmd = new UpdateDepartmentCommand(a.Id, "A", "Phòng A", null, null, ParentId: c.Id);

        var act = async () => await handler.Handle(cmd, CancellationToken.None);

        await act.Should().ThrowAsync<ConflictException>()
            .WithMessage("*sẽ tạo vòng lặp phân cấp*");
    }

    [Fact]
    public async Task Update_SetParentToUnrelatedDepartment_Succeeds()
    {
        // Gán cha hợp lệ (phòng độc lập, KHÔNG con-cháu) → pass guard, persist ParentId.
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var a = Dept("A", "Phòng A");
        var b = Dept("B", "Phòng B");   // độc lập, không quan hệ
        db.Departments.AddRange(a, b);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new UpdateDepartmentCommandHandler(db);
        // Gán A làm con của B — hợp lệ (B không phải con-cháu của A).
        var cmd = new UpdateDepartmentCommand(a.Id, "A", "Phòng A", null, null, ParentId: b.Id);

        await handler.Handle(cmd, CancellationToken.None);

        var reloadA = await db.Departments.AsNoTracking().FirstAsync(x => x.Id == a.Id);
        reloadA.ParentId.Should().Be(b.Id);
    }

    [Fact]
    public async Task Update_ClearParentToNull_Succeeds()
    {
        // ParentId null = promote về root — guard không chạy (chỉ chạy khi có ParentId).
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var a = Dept("A", "Phòng A");
        var b = Dept("B", "Phòng B", a.Id);   // B con của A
        db.Departments.AddRange(a, b);
        await db.SaveChangesAsync(CancellationToken.None);

        var handler = new UpdateDepartmentCommandHandler(db);
        var cmd = new UpdateDepartmentCommand(b.Id, "B", "Phòng B", null, null, ParentId: null);

        await handler.Handle(cmd, CancellationToken.None);

        var reloadB = await db.Departments.AsNoTracking().FirstAsync(x => x.Id == b.Id);
        reloadB.ParentId.Should().BeNull();
    }

    // ---- Seed N active/inactive User gán DepartmentId (đếm org-chart) ----
    private static async Task SeedUsersAsync(IdentityFixture fix, Guid deptId, int count, bool isActive = true)
    {
        var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
        for (var i = 0; i < count; i++)
        {
            // CreateUserAsync luôn set IsActive=true → với inactive, tạo xong update flag.
            var suffix = Guid.NewGuid().ToString("N")[..8];
            var user = await fix.CreateUserAsync(
                $"u-{suffix}@test.local", $"NV {suffix}", departmentId: deptId, roles: System.Array.Empty<string>());
            if (!isActive)
            {
                var tracked = await db.Users.FirstAsync(u => u.Id == user.Id);
                tracked.IsActive = false;
                await db.SaveChangesAsync(CancellationToken.None);
            }
        }
    }
}
