using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Http;
using Moq;
using ROLAC.API.Data;
using ROLAC.API.Data.Interceptors;
using ROLAC.API.DTOs.Expense;
using ROLAC.API.Entities;
using ROLAC.API.Entities.Logging;
using ROLAC.API.Services;
using ROLAC.API.Services.Logging;
using ROLAC.API.Services.Storage;
using ROLAC.API.Tests.TestSupport;
using Xunit;

namespace ROLAC.API.Tests.Services;

public class ExpenseServiceTests
{
    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 static AppDbContext BuildDb(string userId)
    {
        var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) };
        var ctx    = new DefaultHttpContext { User = new(new ClaimsIdentity(claims)) };
        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);
    }

    private static (ExpenseService svc, AppDbContext db, FakeStorage fs) Build(string userId = "u1")
    {
        var db = BuildDb(userId);
        db.Ministries.Add(new Ministry { Id = 1, Name_en = "Worship" });
        db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 1, Name_en = "Equipment" });
        db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Purchase" });
        db.SaveChanges();
        var fs = new FakeStorage();
        return (SvcAs(db, fs, userId), db, fs);
    }

    private static ExpenseService 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 ExpenseService(db, http.Object, fs, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance);
    }

    private static ExpenseService SvcAs(AppDbContext db, FakeStorage fs, string userId, IAuditLogger audit)
    {
        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 ExpenseService(db, http.Object, fs, audit);
    }

    // Builds a service whose principal carries ONLY the "sub" claim (no NameIdentifier),
    // mirroring the real JWT (NameClaimType="sub", MapInboundClaims=false).
    private static ExpenseService SvcWithSubClaim(AppDbContext db, FakeStorage fs, string userId)
    {
        var http = new Mock<IHttpContextAccessor>();
        var ctx  = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim("sub", userId) })) };
        http.Setup(x => x.HttpContext).Returns(ctx);
        return new ExpenseService(db, http.Object, fs, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance);
    }

    private static CreateExpenseRequest Reimb() => new()
    {
        Type = "StaffReimbursement", MinistryId = 1,
        Lines = { new ExpenseLineInput { CategoryGroupId = 1, SubCategoryId = 1, Amount = 45.50m } },
        Description = "Batteries", ExpenseDate = new DateOnly(2026, 5, 28),
    };

    private static UpdateExpenseRequest CloneToUpdate(CreateExpenseRequest r) => new()
    {
        Type = r.Type, MinistryId = r.MinistryId,
        Lines = r.Lines.Select(l => new ExpenseLineInput
        {
            CategoryGroupId = l.CategoryGroupId, SubCategoryId = l.SubCategoryId,
            Amount = l.Amount, FunctionalClass = l.FunctionalClass, Description = l.Description,
        }).ToList(),
        Description = r.Description,
        VendorName = r.VendorName, MemberId = r.MemberId, CheckNumber = r.CheckNumber,
        ExpenseDate = r.ExpenseDate, Notes = r.Notes,
    };

    [Fact]
    public async Task Create_Reimbursement_ResolvesUserId_FromSubClaim()
    {
        // Regression: the real JWT exposes the user id as "sub", not ClaimTypes.NameIdentifier.
        // SubmittedBy must be the sub value (not "system"), or the self-ownership guard breaks.
        var db = BuildDb("ignored");
        db.Ministries.Add(new Ministry { Id = 1, Name_en = "Worship" });
        db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 1, Name_en = "Equipment" });
        db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Purchase" });
        await db.SaveChangesAsync();
        var svc = SvcWithSubClaim(db, new FakeStorage(), "user-guid-123");

        var id = await svc.CreateAsync(Reimb(), isFinance: false);

        var e = await db.Expenses.FindAsync(id);
        Assert.Equal("user-guid-123", e!.SubmittedBy);
    }

    [Fact]
    public async Task Create_Vendor_AsFinance_IsPendingApproval()
    {
        var (svc, db, _) = Build();
        var r = Reimb(); r.Type = "VendorPayment"; r.VendorName = "ABC"; r.CheckNumber = "2051";
        var id = await svc.CreateAsync(r, isFinance: true);
        Assert.Equal("PendingApproval", (await db.Expenses.FindAsync(id))!.Status);
    }

    [Fact]
    public async Task Create_Reimbursement_AsFinance_OnBehalf_IsPendingApproval_AndLinksPickedMember()
    {
        // Finance entering on behalf of a member (member explicitly picked) goes straight to the
        // approval queue and links the picked member.
        var (svc, db, _) = Build();
        db.Members.Add(new Member { Id = 9, FirstName_en = "Pat", LastName_en = "Vendor" });
        await db.SaveChangesAsync();
        var r = Reimb(); r.MemberId = 9;

        var id = await svc.CreateAsync(r, isFinance: true);

        var e = await db.Expenses.FindAsync(id);
        Assert.Equal("PendingApproval", e!.Status);
        Assert.Equal(9, e.MemberId);
    }

    [Fact]
    public async Task Create_Reimbursement_AsFinance_SelfService_LinksCallerMember_AndIsDraft()
    {
        // Regression: a finance/super_admin user filing their OWN reimbursement via "My Reimbursements"
        // sends no MemberId. The entry must link to the caller's own member (so the Payee shows their
        // legal name) and stay a Draft until they explicitly Submit — not jump to PendingApproval with
        // a null member.
        var (svc, db, _) = Build("u1");
        db.Members.Add(new Member { Id = 7, FirstName_en = "Grace", LastName_en = "Lee" });
        db.Users.Add(new AppUser { Id = "u1", MemberId = 7 });
        await db.SaveChangesAsync();

        var id = await svc.CreateAsync(Reimb(), isFinance: true); // no MemberId on the request

        var e = await db.Expenses.FindAsync(id);
        Assert.Equal(7, e!.MemberId);
        Assert.Equal("Draft", e.Status);
    }

    [Fact]
    public async Task Create_Reimbursement_AsMember_IsDraft_WithSubmitter()
    {
        var (svc, db, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        var e = await db.Expenses.FindAsync(id);
        Assert.Equal("Draft", e!.Status);
        Assert.Equal("alice", e.SubmittedBy);
    }

    [Fact]
    public async Task StateMachine_HappyPath_Submit_Approve_Pay()
    {
        var (svc, db, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        Assert.Equal("PendingApproval", (await db.Expenses.FindAsync(id))!.Status);
        await svc.ApproveAsync(id);
        Assert.Equal("Approved", (await db.Expenses.FindAsync(id))!.Status);
        await svc.PayAsync(id, "3001", new DateOnly(2026, 6, 1));
        var paid = await db.Expenses.FindAsync(id);
        Assert.Equal("Paid", paid!.Status);
        Assert.Equal("3001", paid.CheckNumber);
    }

    [Fact]
    public async Task Approve_FromDraft_Throws()
    {
        var (svc, _, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await Assert.ThrowsAsync<InvalidOperationException>(() => svc.ApproveAsync(id));
    }

    [Fact]
    public async Task Reject_RecordsNotes_AndStatus()
    {
        var (svc, db, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        await svc.RejectAsync(id, "Missing receipt");
        var e = await db.Expenses.FindAsync(id);
        Assert.Equal("Rejected", e!.Status);
        Assert.Equal("Missing receipt", e.ReviewNotes);
    }

    [Fact]
    public async Task Update_OthersDraft_AsNonFinance_Throws()
    {
        var (aliceSvc, db, fs) = Build("alice");
        var id = await aliceSvc.CreateAsync(Reimb(), isFinance: false);
        var bobSvc = SvcAs(db, fs, "bob");
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            bobSvc.UpdateAsync(id, CloneToUpdate(Reimb()), isFinance: false));
    }

    [Fact]
    public async Task Update_OwnPendingApproval_AsNonFinance_Succeeds()
    {
        // After Submit a reimbursement sits in PendingApproval; the owner may still correct it.
        var (svc, db, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        Assert.Equal("PendingApproval", (await db.Expenses.FindAsync(id))!.Status);

        var edit = CloneToUpdate(Reimb());
        edit.Lines[0].Amount = 99.99m;
        await svc.UpdateAsync(id, edit, isFinance: false);

        var e = await db.Expenses.FindAsync(id);
        Assert.Equal(99.99m, e!.Amount);
        Assert.Equal("PendingApproval", e.Status);
    }

    [Fact]
    public async Task Update_OwnApproved_AsNonFinance_Throws()
    {
        // Once approved, the owner can no longer edit.
        var (svc, db, fs) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        await SvcAs(db, fs, "finance").ApproveAsync(id);

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            svc.UpdateAsync(id, CloneToUpdate(Reimb()), isFinance: false));
    }

    [Fact]
    public async Task SaveReceipt_OwnPendingApproval_AsNonFinance_Succeeds()
    {
        var (svc, db, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        using var input = new MemoryStream(Encoding.UTF8.GetBytes("img"));
        await svc.SaveReceiptAsync(id, input, "r.jpg", isFinance: false);
        Assert.NotNull(await svc.OpenReceiptAsync(id, isFinance: true));
    }

    [Fact]
    public async Task SoftDelete_HidesFromQueries()
    {
        var (svc, db, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.DeleteAsync(id, isFinance: true);
        Assert.Null(await db.Expenses.FirstOrDefaultAsync(e => e.Id == id));
    }

    [Fact]
    public async Task Create_PersistsFunctionalClass_AndGetReturnsIt()
    {
        var db = BuildDb("u1");
        db.Ministries.Add(new ROLAC.API.Entities.Ministry { Id = 1, Name_en = "Admin" });
        db.ExpenseCategoryGroups.Add(new ROLAC.API.Entities.ExpenseCategoryGroup { Id = 1, Name_en = "Other" });
        db.ExpenseSubCategories.Add(new ROLAC.API.Entities.ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Misc" });
        await db.SaveChangesAsync();
        var svc = SvcAs(db, new FakeStorage(), "u1");

        var id = await svc.CreateAsync(new CreateExpenseRequest
        {
            Type = "VendorPayment", MinistryId = 1,
            Lines = { new ExpenseLineInput { CategoryGroupId = 1, SubCategoryId = 1, Amount = 50m, FunctionalClass = "ManagementGeneral" } },
            Description = "x", ExpenseDate = new DateOnly(2026, 5, 1),
        }, isFinance: true);

        var dto = await svc.GetByIdAsync(id);
        Assert.Equal("ManagementGeneral", dto!.Lines.Single().FunctionalClass);
    }

    [Fact]
    public async Task Create_MultiLine_SetsHeaderTotal_AndRoundTripsLines()
    {
        var (svc, db, _) = Build("u1");
        db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 2, Name_en = "Food & Beverage" });
        db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 2, GroupId = 2, Name_en = "Snacks" });
        await db.SaveChangesAsync();

        var r = new CreateExpenseRequest
        {
            Type = "VendorPayment", MinistryId = 1, VendorName = "Costco",
            Description = "Mixed invoice", ExpenseDate = new DateOnly(2026, 5, 1),
            Lines =
            {
                new ExpenseLineInput { CategoryGroupId = 1, SubCategoryId = 1, Amount = 30m },
                new ExpenseLineInput { CategoryGroupId = 2, SubCategoryId = 2, Amount = 12.50m },
            },
        };
        var id = await svc.CreateAsync(r, isFinance: true);

        Assert.Equal(42.50m, (await db.Expenses.FindAsync(id))!.Amount);
        var dto = await svc.GetByIdAsync(id);
        Assert.Equal(2, dto!.Lines.Count);
        Assert.Equal(42.50m, dto.Amount);
        Assert.Equal(2, dto.LineCount);
    }

    [Fact]
    public async Task Create_WithNoLines_Throws()
    {
        var (svc, _, _) = Build("u1");
        var r = Reimb(); r.Lines.Clear();
        await Assert.ThrowsAsync<InvalidOperationException>(() => svc.CreateAsync(r, isFinance: false));
    }

    [Fact]
    public async Task Update_ReplacesLines_AndRecomputesTotal()
    {
        var (svc, db, _) = Build("alice");
        db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 2, Name_en = "Food & Beverage" });
        db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 2, GroupId = 2, Name_en = "Snacks" });
        await db.SaveChangesAsync();

        var id = await svc.CreateAsync(Reimb(), isFinance: false);

        var edit = CloneToUpdate(Reimb());
        edit.Lines = new()
        {
            new ExpenseLineInput { CategoryGroupId = 1, SubCategoryId = 1, Amount = 10m },
            new ExpenseLineInput { CategoryGroupId = 2, SubCategoryId = 2, Amount = 5m },
        };
        await svc.UpdateAsync(id, edit, isFinance: false);

        Assert.Equal(15m, (await db.Expenses.FindAsync(id))!.Amount);
        Assert.Equal(2, await db.ExpenseLines.CountAsync(l => l.ExpenseId == id));
    }

    [Fact]
    public async Task Receipt_SaveThenOpen_RoundTrips()
    {
        var (svc, _, _) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        using var input = new MemoryStream(Encoding.UTF8.GetBytes("img"));
        await svc.SaveReceiptAsync(id, input, "r.jpg", isFinance: false);
        var got = await svc.OpenReceiptAsync(id, isFinance: true);
        Assert.NotNull(got);
    }

    [Fact]
    public async Task Reject_WritesAuditEntry_WithReason()
    {
        var (svc, db, fs) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);

        var audit = new CapturingAuditLogger();
        await SvcAs(db, fs, "finance", audit).RejectAsync(id, "Receipt unclear, please retake");

        var entry = Assert.Single(audit.Entries);
        Assert.Equal(AuditActions.ExpenseRejected, entry.Action);
        Assert.Equal(AuditCategories.Business, entry.Category);
        Assert.Equal(nameof(ROLAC.API.Entities.Expense), entry.EntityName);
        Assert.Equal(id.ToString(), entry.EntityId);
        Assert.Contains("Receipt unclear", entry.Summary);
    }

    [Fact]
    public async Task Resubmit_FromRejected_ReturnsToPending_AndClearsReview()
    {
        var (svc, db, fs) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        await SvcAs(db, fs, "finance").RejectAsync(id, "Receipt missing");

        // Owner fixes the issue and re-submits.
        await svc.SubmitAsync(id);

        var e = await db.Expenses.FindAsync(id);
        Assert.Equal("PendingApproval", e!.Status);
        Assert.Null(e.ReviewedBy);
        Assert.Null(e.ReviewedAt);
        Assert.Null(e.ReviewNotes);
    }

    [Fact]
    public async Task Update_OwnRejected_AsNonFinance_Succeeds()
    {
        // A rejected reimbursement can be corrected by its owner before re-submitting.
        var (svc, db, fs) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        await SvcAs(db, fs, "finance").RejectAsync(id, "Amount does not match receipt");

        var edit = CloneToUpdate(Reimb());
        edit.Lines[0].Amount = 77.77m;
        await svc.UpdateAsync(id, edit, isFinance: false);

        var e = await db.Expenses.FindAsync(id);
        Assert.Equal(77.77m, e!.Amount);
        Assert.Equal("Rejected", e.Status);
    }

    [Fact]
    public async Task SaveReceipt_OwnRejected_AsNonFinance_Succeeds()
    {
        var (svc, db, fs) = Build("alice");
        var id = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(id);
        await SvcAs(db, fs, "finance").RejectAsync(id, "Receipt unclear, please retake");

        using var input = new MemoryStream(Encoding.UTF8.GetBytes("img"));
        await svc.SaveReceiptAsync(id, input, "retake.jpg", isFinance: false);
        Assert.NotNull(await svc.OpenReceiptAsync(id, isFinance: true));
    }

    [Fact]
    public async Task GetById_ResolvesReviewerName_MemberFullName_EmailFallback()
    {
        var (svc, db, fs) = Build("alice");
        // Reviewer linked to a member → shows the member's full name.
        db.Members.Add(new Member { Id = 5, FirstName_en = "Sam", LastName_en = "Approver" });
        db.Users.Add(new AppUser { Id = "reviewer-with-member", MemberId = 5 });
        // Reviewer with no member → falls back to email.
        db.Users.Add(new AppUser { Id = "reviewer-no-member", Email = "nomember@church.org" });
        await db.SaveChangesAsync();

        var withMember = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(withMember);
        await SvcAs(db, fs, "reviewer-with-member").ApproveAsync(withMember);
        Assert.Equal("Sam Approver", (await svc.GetByIdAsync(withMember))!.ReviewedByName);

        var noMember = await svc.CreateAsync(Reimb(), isFinance: false);
        await svc.SubmitAsync(noMember);
        await SvcAs(db, fs, "reviewer-no-member").RejectAsync(noMember, "Duplicate submission");
        Assert.Equal("nomember@church.org", (await svc.GetByIdAsync(noMember))!.ReviewedByName);
    }
}
