using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Moq;
using ROLAC.API.Data;
using ROLAC.API.Data.Interceptors;
using ROLAC.API.Entities;
using Xunit;

namespace ROLAC.API.Tests.Services;

public class DbSeederForm990Tests
{
    private static AppDbContext BuildDb()
    {
        var ctx  = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "seed") })) };
        var mock = new Mock<IHttpContextAccessor>();
        mock.Setup(x => x.HttpContext).Returns(ctx);
        return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .AddInterceptors(new AuditSaveChangesInterceptor(new ROLAC.API.Services.Logging.CurrentUserAccessor(mock.Object))).Options);
    }

    [Fact]
    public async Task SeedExpenseCategories_AddsNewGroups_RenamesDuplicates_AndIsIdempotent()
    {
        using var db = BuildDb();
        var fnb = new ExpenseCategoryGroup { Name_en = "Food & Beverage", Name_zh = "餐飲", SortOrder = 3 };
        db.ExpenseCategoryGroups.Add(fnb);
        await db.SaveChangesAsync();
        db.ExpenseSubCategories.Add(new ExpenseSubCategory { GroupId = fnb.Id, Name_en = "Consumables", Name_zh = "消耗品" });
        await db.SaveChangesAsync();

        await DbSeeder.SeedExpenseCategoriesAsync(db);
        await DbSeeder.SeedExpenseCategoriesAsync(db); // idempotent second run

        var groups = await db.ExpenseCategoryGroups.ToListAsync();
        Assert.Contains(groups, g => g.Name_en == "Professional Services");
        Assert.Contains(groups, g => g.Name_en == "Information Technology");
        Assert.Contains(groups, g => g.Name_en == "Finance & Banking");

        var fnbSubs = await db.ExpenseSubCategories.Where(s => s.GroupId == fnb.Id).ToListAsync();
        Assert.DoesNotContain(fnbSubs, s => s.Name_en == "Consumables");
        Assert.Contains(fnbSubs, s => s.Name_en == "Disposable Tableware");

        Assert.Single(groups, g => g.Name_en == "Professional Services");
    }

    [Fact]
    public async Task SeedMinistries_SetsAdministrationToManagementGeneral_OthersProgram()
    {
        using var db = BuildDb();
        await DbSeeder.SeedMinistriesAsync(db);

        var admin = await db.Ministries.FirstAsync(m => m.Name_en == "Administration");
        var worship = await db.Ministries.FirstAsync(m => m.Name_en == "Worship");
        Assert.Equal("ManagementGeneral", admin.DefaultFunctionalClass);
        Assert.Equal("Program", worship.DefaultFunctionalClass);

        // Activity/shepherding ministries are an attribution axis only; they default to Program
        // so adding them never distorts the 990 functional columns.
        var cellGroups = await db.Ministries.FirstAsync(m => m.Name_en == "Cell Groups");
        var specialEvents = await db.Ministries.FirstAsync(m => m.Name_en == "Special Events");
        Assert.Equal("Program", cellGroups.DefaultFunctionalClass);
        Assert.Equal("Program", specialEvents.DefaultFunctionalClass);
    }

    [Fact]
    public async Task SeedForm990Lines_CreatesCatalog_AndMapsKnownSubcategories()
    {
        using var db = BuildDb();
        await DbSeeder.SeedExpenseCategoriesAsync(db);
        await DbSeeder.SeedForm990ExpenseLinesAsync(db);
        await DbSeeder.SeedForm990ExpenseLinesAsync(db); // idempotent

        Assert.Equal(1, await db.Form990ExpenseLines.CountAsync(l => l.LineCode == "7"));
        Assert.True(await db.Form990ExpenseLines.AnyAsync(l => l.LineCode == "24"));

        var salary = await db.ExpenseSubCategories.Include(s => s.Form990Line)
            .FirstAsync(s => s.Name_en == "Salary & Wages");
        Assert.Equal("7", salary.Form990Line!.LineCode);

        var audit = await db.ExpenseSubCategories.Include(s => s.Form990Line)
            .FirstAsync(s => s.Name_en == "Accounting & Audit");
        Assert.Equal("11c", audit.Form990Line!.LineCode);
    }

    [Fact]
    public async Task SeedForm990Lines_MapsAuditCorrectedSubcategories_OffTheLine24CatchAll()
    {
        using var db = BuildDb();
        await DbSeeder.SeedExpenseCategoriesAsync(db);
        await DbSeeder.SeedForm990ExpenseLinesAsync(db);

        async Task<string> CodeOf(string subEn) =>
            (await db.ExpenseSubCategories.Include(s => s.Form990Line)
                .FirstAsync(s => s.Name_en == subEn)).Form990Line!.LineCode;

        // Newly mapped subcategories that previously fell through to line 24.
        Assert.Equal("13", await CodeOf("Bank & Processing Fees"));
        Assert.Equal("13", await CodeOf("Rental"));
        Assert.Equal("13", await CodeOf("Maintenance & Repair"));
        Assert.Equal("13", await CodeOf("Cleaning Supplies"));
        Assert.Equal("13", await CodeOf("Craft Supplies"));
        // Building repairs & maintenance are part of Occupancy (line 16), not equipment (line 13).
        Assert.Equal("16", await CodeOf("Repairs & Maintenance"));
        // Appreciation/outreach gifts are deliberately mapped to Other (line 24), not left unmapped.
        Assert.Equal("24", await CodeOf("Gifts"));
        // Visitation is a travel/program cost, not a grant to an individual.
        Assert.Equal("17", await CodeOf("Visit Expenses"));
        // Missions support paid to individual missionaries → line 2, not line 1 (organizations).
        Assert.Equal("2",  await CodeOf("Missionary Support"));
    }

    [Fact]
    public async Task SeedForm990Lines_RemapsExistingBadMapping_ButNotAdminOverride()
    {
        using var db = BuildDb();
        await DbSeeder.SeedExpenseCategoriesAsync(db);
        await DbSeeder.SeedForm990ExpenseLinesAsync(db);

        // Simulate a database seeded by the OLD code: Visit Expenses on line 2, Missionary
        // Support on line 1. Also simulate an admin who deliberately moved one elsewhere.
        var lineByCode = await db.Form990ExpenseLines.ToDictionaryAsync(l => l.LineCode, l => l.Id);
        var visit = await db.ExpenseSubCategories.FirstAsync(s => s.Name_en == "Visit Expenses");
        var missionary = await db.ExpenseSubCategories.FirstAsync(s => s.Name_en == "Missionary Support");
        var transfer = await db.ExpenseSubCategories.FirstAsync(s => s.Name_en == "Offering Transfer");
        visit.Form990LineId      = lineByCode["2"];   // old (wrong) value → should be corrected
        missionary.Form990LineId = lineByCode["1"];   // old (wrong) value → should be corrected
        transfer.Form990LineId   = lineByCode["24"];  // admin override → must be left alone
        await db.SaveChangesAsync();

        await DbSeeder.SeedForm990ExpenseLinesAsync(db);

        await db.Entry(visit).ReloadAsync();
        await db.Entry(missionary).ReloadAsync();
        await db.Entry(transfer).ReloadAsync();
        Assert.Equal(lineByCode["17"], visit.Form990LineId);      // corrected 2 → 17
        Assert.Equal(lineByCode["2"],  missionary.Form990LineId); // corrected 1 → 2
        Assert.Equal(lineByCode["24"], transfer.Form990LineId);   // admin edit preserved
    }
}
