using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Moq;
using ROLAC.API.Data;
using ROLAC.API.Data.Interceptors;
using ROLAC.API.DTOs.Disbursement;
using ROLAC.API.Entities;
using ROLAC.API.Services;
using ROLAC.API.Services.Disbursement;
using ROLAC.API.Services.Storage;
using Xunit;

namespace ROLAC.API.Tests.Services;

public class DisbursementServiceTests
{
    private sealed class FakeStorage : IFileStorage
    {
        public Dictionary<string, byte[]> Files = new();
        public Task<string> SaveAsync(Stream c, string p, CancellationToken ct = default)
        { using var ms = new MemoryStream(); c.CopyTo(ms); Files[p] = ms.ToArray(); return Task.FromResult(p); }
        public Task<Stream?> OpenReadAsync(string p, CancellationToken ct = default)
        => Task.FromResult<Stream?>(Files.TryGetValue(p, out var b) ? new MemoryStream(b) : null);
        public Task DeleteAsync(string p, CancellationToken ct = default) { Files.Remove(p); return Task.CompletedTask; }
    }

    private sealed class FakePrint : ICheckPrintService
    {
        public CheckPrintModel? LastReceiptModel;

        public Task<Stream> RenderPdfAsync(CheckPrintModel model)
            => Task.FromResult<Stream>(new MemoryStream(Encoding.UTF8.GetBytes("pdf")));

        public Task<Stream> RenderReceiptPdfAsync(CheckPrintModel model)
        {
            LastReceiptModel = model;
            return Task.FromResult<Stream>(new MemoryStream(Encoding.UTF8.GetBytes("receipt-pdf")));
        }
    }

    private static AppDbContext BuildDb(string userId)
    {
        var ctx  = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })) };
        var mock = new Mock<IHttpContextAccessor>();
        mock.Setup(x => x.HttpContext).Returns(ctx);
        return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
            .AddInterceptors(new AuditSaveChangesInterceptor(new ROLAC.API.Services.Logging.CurrentUserAccessor(mock.Object))).Options);
    }

    private static DisbursementService SvcAs(AppDbContext db, FakeStorage fs, string userId)
    {
        var http = new Mock<IHttpContextAccessor>();
        var ctx  = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })) };
        http.Setup(x => x.HttpContext).Returns(ctx);
        return new DisbursementService(db, http.Object, fs, new FakePrint(), ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance);
    }

    private static (DisbursementService svc, AppDbContext db, FakeStorage fs) Build(string userId = "fin")
    {
        var db = BuildDb(userId);
        db.ChurchProfiles.Add(new ChurchProfile { Id = 1, Name = "ROLAC", NextCheckNumber = 1001 });
        db.Members.Add(new Member { Id = 1, FirstName_en = "John", LastName_en = "Doe", Address = "1 Main St", City = "Arcadia", State = "CA", ZipCode = "91006" });
        db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 1, Name_en = "Equipment" });
        db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 2, Name_en = "Food & Beverage" });
        db.SaveChanges();
        var fs = new FakeStorage();
        return (SvcAs(db, fs, userId), db, fs);
    }

    private static Expense Approved(string type, decimal amount, int? memberId = null, string? vendor = null) => new()
    {
        Type = type, Status = "Approved", Amount = amount, Description = $"{type} {amount}",
        MinistryId = 1, ExpenseDate = new DateOnly(2026, 6, 1),
        MemberId = memberId, VendorName = vendor,
        Lines = { new ExpenseLine { CategoryGroupId = 1, SubCategoryId = 1, Amount = amount } },
    };

    [Fact]
    public async Task GroupedWorklist_BundlesSamePayee()
    {
        var (svc, db, _) = Build();
        db.Expenses.AddRange(
            Approved("StaffReimbursement", 10m, memberId: 1),
            Approved("StaffReimbursement", 15m, memberId: 1),
            Approved("VendorPayment", 30m, vendor: "Acme"));
        await db.SaveChangesAsync();

        var groups = await svc.GetApprovedUnpaidGroupedAsync();

        Assert.Equal(2, groups.Count);
        var member = groups.Single(g => g.PayeeType == "Member");
        Assert.Equal(25m, member.TotalAmount);
        Assert.Equal(2, member.Lines.Count);
        Assert.Equal("John Doe", member.PayeeName);
        Assert.Equal("1 Main St", member.Address);
    }

    [Fact]
    public async Task GroupedWorklist_MultiCategoryExpense_ShowsMultipleLabel()
    {
        var (svc, db, _) = Build();
        db.Expenses.Add(new Expense
        {
            Type = "VendorPayment", Status = "Approved", Amount = 50m, Description = "mixed invoice",
            MinistryId = 1, ExpenseDate = new DateOnly(2026, 6, 1), VendorName = "Costco",
            Lines =
            {
                new ExpenseLine { CategoryGroupId = 1, SubCategoryId = 1, Amount = 30m },
                new ExpenseLine { CategoryGroupId = 2, SubCategoryId = 2, Amount = 20m },
            },
        });
        await db.SaveChangesAsync();

        var groups = await svc.GetApprovedUnpaidGroupedAsync();

        var line = groups.Single(g => g.PayeeType == "Vendor").Lines.Single();
        Assert.Equal("Multiple / 多類別", line.CategoryName);
    }

    [Fact]
    public async Task Issue_CreatesOneCheckPerPayee_MarksPaid_SequentialNumbers()
    {
        var (svc, db, _) = Build();
        var e1 = Approved("StaffReimbursement", 10m, memberId: 1);
        var e2 = Approved("StaffReimbursement", 15m, memberId: 1);
        var e3 = Approved("VendorPayment", 30m, vendor: "Acme");
        db.Expenses.AddRange(e1, e2, e3);
        await db.SaveChangesAsync();

        var req = new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees =
            [
                new() { PayeeType = "Member", MemberId = 1, PayeeName = "John Doe", ExpenseIds = [e1.Id, e2.Id] },
                new() { PayeeType = "Vendor", VendorKey = "acme", PayeeName = "Acme", ExpenseIds = [e3.Id] },
            ],
        };

        var result = await svc.IssueChecksAsync(req);

        Assert.Equal(2, result.Created.Count);
        Assert.Equal(new[] { "1001", "1002" }, result.Created.Select(c => c.CheckNumber).ToArray());
        Assert.All(await db.Expenses.ToListAsync(), e => Assert.Equal("Paid", e.Status));
        var memberCheck = await db.Checks.FirstAsync(c => c.PayeeType == "Member");
        Assert.Equal(25m, memberCheck.Amount);
        Assert.Equal(1003, (await db.ChurchProfiles.FirstAsync()).NextCheckNumber);
    }

    [Fact]
    public async Task Issue_RejectsNonApprovedExpense()
    {
        var (svc, db, _) = Build();
        var e = Approved("VendorPayment", 30m, vendor: "Acme");
        e.Status = "Draft";
        db.Expenses.Add(e);
        await db.SaveChangesAsync();

        var req = new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees = [new() { PayeeType = "Vendor", PayeeName = "Acme", ExpenseIds = [e.Id] }],
        };

        await Assert.ThrowsAsync<InvalidOperationException>(() => svc.IssueChecksAsync(req));
    }

    [Fact]
    public async Task Void_RevertsExpensesToApproved()
    {
        var (svc, db, _) = Build();
        var e = Approved("VendorPayment", 30m, vendor: "Acme");
        db.Expenses.Add(e);
        await db.SaveChangesAsync();
        var result = await svc.IssueChecksAsync(new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees = [new() { PayeeType = "Vendor", PayeeName = "Acme", ExpenseIds = [e.Id] }],
        });
        var checkId = result.Created[0].CheckId;

        await svc.VoidAsync(checkId, "wrong amount");

        var check = await db.Checks.FirstAsync(c => c.Id == checkId);
        Assert.Equal("Voided", check.Status);
        var reverted = await db.Expenses.FirstAsync(x => x.Id == e.Id);
        Assert.Equal("Approved", reverted.Status);
        Assert.Null(reverted.CheckNumber);
        Assert.Null(reverted.PaidAt);
    }

    [Fact]
    public async Task Acknowledge_StoresSignatureAndTimestamp()
    {
        var (svc, db, fs) = Build();
        var e = Approved("VendorPayment", 30m, vendor: "Acme");
        db.Expenses.Add(e);
        await db.SaveChangesAsync();
        var result = await svc.IssueChecksAsync(new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees = [new() { PayeeType = "Vendor", PayeeName = "Acme", ExpenseIds = [e.Id] }],
        });
        var checkId = result.Created[0].CheckId;

        using var img = new MemoryStream(Encoding.UTF8.GetBytes("png-bytes"));
        await svc.AcknowledgeReceiptAsync(checkId, img, "sig.png", "Acme Rep");

        var check = await db.Checks.FirstAsync(c => c.Id == checkId);
        Assert.NotNull(check.ReceiptSignedAt);
        Assert.Equal("Acme Rep", check.ReceiptSignedName);
        Assert.NotNull(check.ReceiptSignatureBlobPath);
        Assert.Single(fs.Files);
    }

    private static (DisbursementService svc, AppDbContext db, FakeStorage fs, FakePrint print) BuildWithPrint(string userId = "fin")
    {
        var db = BuildDb(userId);
        db.ChurchProfiles.Add(new ChurchProfile { Id = 1, Name = "ROLAC", NextCheckNumber = 1001 });
        db.SaveChanges();
        var fs = new FakeStorage();
        var print = new FakePrint();
        var http = new Mock<IHttpContextAccessor>();
        var ctx = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })) };
        http.Setup(x => x.HttpContext).Returns(ctx);
        return (new DisbursementService(db, http.Object, fs, print, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance), db, fs, print);
    }

    [Fact]
    public async Task ReceiptPdf_NullWhenNotSigned()
    {
        var (svc, db, _, _) = BuildWithPrint();
        var e = Approved("VendorPayment", 30m, vendor: "Acme");
        db.Expenses.Add(e);
        await db.SaveChangesAsync();
        var result = await svc.IssueChecksAsync(new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees = [new() { PayeeType = "Vendor", PayeeName = "Acme", ExpenseIds = [e.Id] }],
        });

        var receipt = await svc.RenderReceiptPdfAsync(result.Created[0].CheckId);

        Assert.Null(receipt);
    }

    [Fact]
    public async Task ReceiptPdf_AfterSigning_RendersWithSignatureBytes()
    {
        var (svc, db, _, print) = BuildWithPrint();
        var e = Approved("VendorPayment", 30m, vendor: "Acme");
        db.Expenses.Add(e);
        await db.SaveChangesAsync();
        var result = await svc.IssueChecksAsync(new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees = [new() { PayeeType = "Vendor", PayeeName = "Acme", ExpenseIds = [e.Id] }],
        });
        var checkId = result.Created[0].CheckId;
        using var img = new MemoryStream(Encoding.UTF8.GetBytes("png-bytes"));
        await svc.AcknowledgeReceiptAsync(checkId, img, "sig.png", "Acme Rep");

        var receipt = await svc.RenderReceiptPdfAsync(checkId);

        Assert.NotNull(receipt);
        Assert.Equal("receipt-1001.pdf", receipt!.Value.fileName);
        Assert.NotNull(print.LastReceiptModel);
        Assert.NotNull(print.LastReceiptModel!.SignatureImage);
        Assert.Equal("Acme Rep", print.LastReceiptModel.Check.ReceiptSignedName);
    }

    [Fact]
    public async Task Acknowledge_VoidedCheck_Throws()
    {
        var (svc, db, _) = Build();
        var e = Approved("VendorPayment", 30m, vendor: "Acme");
        db.Expenses.Add(e);
        await db.SaveChangesAsync();
        var result = await svc.IssueChecksAsync(new IssueChecksRequest
        {
            CheckDate = new DateOnly(2026, 6, 20),
            Payees = [new() { PayeeType = "Vendor", PayeeName = "Acme", ExpenseIds = [e.Id] }],
        });
        var checkId = result.Created[0].CheckId;
        await svc.VoidAsync(checkId, null);

        using var img = new MemoryStream(Encoding.UTF8.GetBytes("png"));
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => svc.AcknowledgeReceiptAsync(checkId, img, "sig.png", "X"));
    }
}
