using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Hrm;
using SolutionErp.Domain.Hrm;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Application;

// Coverage gap #1 (CRITICAL — S35 backlog, closed S45 2026-06-01) — test-AFTER shipped code.
// Holiday composite UNIQUE (Year, Date) guard trong CreateHolidayHandler / UpdateHolidayHandler
// (HrmConfigFeatures.cs Region 2 L157-208). Handlers chỉ cần IApplicationDbContext (no ICurrentUser).
//
// CHỐT theo CODE (single source of truth):
//   - Conflict key = (Year, Date) AND !IsDeleted. Create: AnyAsync(Year==r.Year && Date==r.Date && !IsDeleted).
//   - Update: short-circuit guard `entity.Year != req.Year || entity.Date != req.Date` TRƯỚC khi
//     query conflict (+ loại trừ chính nó qua `Id != req.Id`). Giữ nguyên (Year,Date) → KHÔNG false-positive.
//   - DateOnly. NotFoundException("Holiday", id) khi Id không tồn tại → message "Holiday với id '...' không tồn tại."
//   - Soft-deleted row (IsDeleted=true) bị loại khỏi conflict check (app-level).
//
// ✅ DRIFT RESOLVED (S45 Mig 43 `FilterHolidayUniqueIndexByIsDeleted`):
//   App-level conflict check `!IsDeleted` (soft-deleted slot reusable) trước đây MÂU THUẪN với
//   DB-level UNIQUE `(Year, Date)` KHÔNG filter → recreate trên slot soft-deleted ném DbUpdateException(500).
//   Fix: HolidayConfiguration thêm `.HasFilter("[IsDeleted] = 0")` (khớp pattern Catalogs/Contract/PE).
//   Giờ soft-deleted slot reusable đúng intent → Case 7 assert SUCCESS (xem dưới).
//
// FK note: Holiday KHÔNG có FK ra entity khác (catalog độc lập) → seed trực tiếp, no parent.
// AuditingInterceptor KHÔNG wire trong test fixture → set IsDeleted=true thủ công cho case soft-delete.
public class HrmConfigHolidayTests
{
    private static readonly DateOnly Jan1 = new(2026, 1, 1);

    private static Holiday BuildHoliday(int year, DateOnly date, string name, bool isDeleted = false)
        => new()
        {
            Id = Guid.NewGuid(),
            Year = year,
            Date = date,
            Name = name,
            IsRecurring = false,
            IsPaid = true,
            IsActive = true,
            IsDeleted = isDeleted,
        };

    private static async Task<Holiday> SeedHolidayAsync(
        TestApplicationDbContext db, int year, DateOnly date, string name, bool isDeleted = false)
    {
        var h = BuildHoliday(year, date, name, isDeleted);
        db.Holidays.Add(h);
        await db.SaveChangesAsync(CancellationToken.None);
        return h;
    }

    // ============ Case 1: Create duplicate (Year, Date) → ConflictException ============

    [Fact]
    public async Task CreateHoliday_DuplicateYearDate_ThrowsConflict_NoSecondRow()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        await SeedHolidayAsync(db, 2026, Jan1, "Tết Dương lịch");

        var act = async () => await new CreateHolidayHandler(db)
            .Handle(new CreateHolidayCommand(2026, Jan1, "Tết Dương lịch (trùng)", false, true, null),
                CancellationToken.None);

        await act.Should().ThrowAsync<ConflictException>()
            .WithMessage("*Ngày lễ năm 2026*01/01/2026*đã tồn tại*");
        (await db.Holidays.CountAsync(x => x.Year == 2026 && x.Date == Jan1))
            .Should().Be(1, "conflict chặn trước Add — chỉ 1 row tồn tại");
    }

    // ============ Case 2: Same Date khác Year → succeeds (Year là phần của key) ============

    [Fact]
    public async Task CreateHoliday_SameDate_DifferentYear_Succeeds()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        await SeedHolidayAsync(db, 2026, Jan1, "Tết Dương lịch 2026");

        // (2027, Jan 1) — cùng Date nhưng khác Year → KHÔNG conflict.
        var id = await new CreateHolidayHandler(db)
            .Handle(new CreateHolidayCommand(2027, new DateOnly(2027, 1, 1), "Tết Dương lịch 2027", true, true, null),
                CancellationToken.None);

        id.Should().NotBeEmpty();
        (await db.Holidays.CountAsync()).Should().Be(2, "Year là phần composite key → 2 row tồn tại độc lập");
    }

    // ============ Case 3: Update B → slot A đang chiếm → ConflictException ============

    [Fact]
    public async Task UpdateHoliday_ToSlotOccupiedByOther_ThrowsConflict()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        var a = await SeedHolidayAsync(db, 2026, Jan1, "Ngày lễ A");
        var b = await SeedHolidayAsync(db, 2026, new DateOnly(2026, 2, 2), "Ngày lễ B");

        // Update B → (2026, Jan 1) đang bị A chiếm → conflict.
        var act = async () => await new UpdateHolidayHandler(db)
            .Handle(new UpdateHolidayCommand(b.Id, 2026, Jan1, "Ngày lễ B đổi", false, true, true, null),
                CancellationToken.None);

        await act.Should().ThrowAsync<ConflictException>()
            .WithMessage("*Ngày lễ năm 2026*01/01/2026*đã tồn tại*");

        // A giữ nguyên (no mutation lọt qua).
        var aReloaded = await db.Holidays.AsNoTracking().SingleAsync(x => x.Id == a.Id);
        aReloaded.Date.Should().Be(Jan1);
    }

    // ============ Case 4 ⭐: Update giữ nguyên (Year, Date), đổi mỗi Name → succeeds ============
    // Property tinh tế nhất: short-circuit guard tránh false-positive self-conflict.

    [Fact]
    public async Task UpdateHoliday_SameYearDate_ChangeNameOnly_Succeeds_NoSelfConflict()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        var a = await SeedHolidayAsync(db, 2026, Jan1, "Tên cũ");

        // Year + Date KHÔNG đổi → guard `entity.Year != req.Year || entity.Date != req.Date` = false
        // → KHÔNG chạy conflict query → KHÔNG self-conflict dù tồn tại đúng row đó.
        var act = async () => await new UpdateHolidayHandler(db)
            .Handle(new UpdateHolidayCommand(a.Id, 2026, Jan1, "Tên mới", true, false, true, "ghi chú mới"),
                CancellationToken.None);

        await act.Should().NotThrowAsync("đổi Name khi giữ (Year,Date) KHÔNG được tính là trùng chính nó");

        var reloaded = await db.Holidays.AsNoTracking().SingleAsync(x => x.Id == a.Id);
        reloaded.Name.Should().Be("Tên mới");
        reloaded.IsRecurring.Should().BeTrue("payload mới ghi đè field khác");
        reloaded.IsPaid.Should().BeFalse();
        reloaded.Description.Should().Be("ghi chú mới");
    }

    // ============ Case 5: Update sang slot trống → succeeds ============

    [Fact]
    public async Task UpdateHoliday_ToEmptySlot_Succeeds()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        var a = await SeedHolidayAsync(db, 2026, Jan1, "Ngày lễ A");

        // (2026, Mar 3) chưa ai chiếm → move OK.
        var mar3 = new DateOnly(2026, 3, 3);
        await new UpdateHolidayHandler(db)
            .Handle(new UpdateHolidayCommand(a.Id, 2026, mar3, "Ngày lễ A dời", false, true, true, null),
                CancellationToken.None);

        var reloaded = await db.Holidays.AsNoTracking().SingleAsync(x => x.Id == a.Id);
        reloaded.Date.Should().Be(mar3);
        reloaded.Year.Should().Be(2026);
    }

    // ============ Case 6: Update Id không tồn tại → NotFoundException ============

    [Fact]
    public async Task UpdateHoliday_NonexistentId_ThrowsNotFound()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        var ghost = Guid.NewGuid();

        var act = async () => await new UpdateHolidayHandler(db)
            .Handle(new UpdateHolidayCommand(ghost, 2026, Jan1, "Không tồn tại", false, true, true, null),
                CancellationToken.None);

        await act.Should().ThrowAsync<NotFoundException>()
            .WithMessage($"*Holiday*{ghost}*không tồn tại*");
    }

    // ============ Case 7 ✅: Create trên slot soft-deleted → SUCCESS (Mig 43 filtered unique) ============
    // Sau Mig 43 (.HasFilter("[IsDeleted] = 0")): UNIQUE (Year,Date) chỉ tính row chưa xoá →
    // soft-deleted slot reusable, khớp app-level !IsDeleted intent + pattern Catalogs/Contract/PE.

    [Fact]
    public async Task CreateHoliday_OnSoftDeletedSlot_Succeeds_FilteredUniqueAllowsReuse()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;
        // Seed 1 row (2026, Jan 1) đã soft-delete (IsDeleted=true).
        await SeedHolidayAsync(db, 2026, Jan1, "Lễ cũ đã xoá", isDeleted: true);

        var id = await new CreateHolidayHandler(db)
            .Handle(new CreateHolidayCommand(2026, Jan1, "Lễ mới", false, true, null), CancellationToken.None);

        id.Should().NotBeEmpty("filtered unique index không tính row đã soft-delete → slot reusable");
        // 2 row tồn tại: 1 soft-deleted gốc + 1 active mới; chỉ 1 active chiếm slot.
        (await db.Holidays.CountAsync(x => x.Year == 2026 && x.Date == Jan1 && !x.IsDeleted))
            .Should().Be(1, "chỉ 1 row active chiếm slot");
        (await db.Holidays.CountAsync(x => x.Year == 2026 && x.Date == Jan1))
            .Should().Be(2, "soft-deleted gốc giữ lại cho audit + active mới");
    }
}
