using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Infrastructure.Services;

namespace PowderCoating.UnitTests;

public class LedgerServiceTests
{
    private static readonly DateTime PeriodStart = new DateTime(2026, 4, 1);
    private static readonly DateTime PeriodEnd   = new DateTime(2026, 4, 30);
    private static readonly DateTime InPeriod    = new DateTime(2026, 4, 15);

    // ── Returns null for unknown account ─────────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_WhenAccountDoesNotExist_ReturnsNull()
    {
        await using var context = CreateContext();
        var service = CreateService(context);

        var result = await service.GetAccountLedgerAsync(999, PeriodStart, PeriodEnd);

        Assert.Null(result);
    }

    // ── Source 1: customer payment deposited (DEBIT) ─────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source1_CustomerPaymentDeposited_CreatesDebitEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Checking);
        SeedInvoice(context, id: 99);
        context.Payments.Add(new Payment
        {
            Id = 1, CompanyId = 1,
            InvoiceId = 99,
            DepositAccountId = 1,
            Amount = 250m,
            PaymentDate = InPeriod,
            PaymentMethod = PaymentMethod.Check
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Customer Payment", entry.Source);
        Assert.Equal(250m, entry.Debit);
        Assert.Equal(0m, entry.Credit);
    }

    // ── Source 2: expense paid FROM account (CREDIT) ──────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source2_ExpensePaidFromAccount_CreatesCreditEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Checking);
        context.Expenses.Add(new Expense
        {
            Id = 1, CompanyId = 1,
            ExpenseNumber = "EXP-001",
            PaymentAccountId = 1,     // Checking account pays the expense
            ExpenseAccountId = 999,   // Different account — so Source 6 does not also fire
            Amount = 80m,
            Date = InPeriod,
            PaymentMethod = PaymentMethod.Check
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Expense", entry.Source);
        Assert.Equal(0m, entry.Debit);
        Assert.Equal(80m, entry.Credit);
    }

    // ── Source 3: bill payment made FROM account (CREDIT) ─────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source3_BillPaymentFromAccount_CreatesCreditEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Checking);
        SeedBill(context, id: 99);    // Include(bp => bp.Bill) requires the Bill to exist
        context.BillPayments.Add(new BillPayment
        {
            Id = 1, CompanyId = 1,
            PaymentNumber = "BP-001",
            BillId = 99,
            VendorId = 1,
            BankAccountId = 1,        // Checking account used to pay the bill
            Amount = 150m,
            PaymentDate = InPeriod,
            PaymentMethod = PaymentMethod.Check
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Bill Payment", entry.Source);
        Assert.Equal(0m, entry.Debit);
        Assert.Equal(150m, entry.Credit);
    }

    // ── Source 4: invoice revenue line items (CREDIT) ─────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source4_InvoiceLineItem_CreatesCreditEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Sales);
        context.Invoices.Add(new Invoice
        {
            Id = 10, CompanyId = 1,
            InvoiceNumber = "INV-001",
            CustomerId = 1,
            Status = InvoiceStatus.Sent,
            InvoiceDate = InPeriod,
            Total = 500m
        });
        context.InvoiceItems.Add(new InvoiceItem
        {
            Id = 1, CompanyId = 1,
            InvoiceId = 10,
            RevenueAccountId = 1,
            Description = "Powder Coating",
            Quantity = 1, UnitPrice = 500m, TotalPrice = 500m
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Invoice", entry.Source);
        Assert.Equal(0m, entry.Debit);
        Assert.Equal(500m, entry.Credit);
    }

    [Fact]
    public async Task GetAccountLedgerAsync_Source4_DraftAndVoidedInvoicesExcluded()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Sales);
        context.Invoices.AddRange(
            new Invoice { Id = 10, CompanyId = 1, InvoiceNumber = "INV-DRAFT", CustomerId = 1, Status = InvoiceStatus.Draft,  InvoiceDate = InPeriod },
            new Invoice { Id = 11, CompanyId = 1, InvoiceNumber = "INV-VOID",  CustomerId = 1, Status = InvoiceStatus.Voided, InvoiceDate = InPeriod });
        context.InvoiceItems.AddRange(
            new InvoiceItem { Id = 1, CompanyId = 1, InvoiceId = 10, RevenueAccountId = 1, Description = "D", Quantity = 1, UnitPrice = 100m, TotalPrice = 100m },
            new InvoiceItem { Id = 2, CompanyId = 1, InvoiceId = 11, RevenueAccountId = 1, Description = "V", Quantity = 1, UnitPrice = 100m, TotalPrice = 100m });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Empty(ledger!.Entries);
    }

    // ── Source 5: sales tax collected (CREDIT) ────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source5_SalesTaxCollected_CreatesCreditEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.OtherCurrentLiability);
        context.Invoices.Add(new Invoice
        {
            Id = 10, CompanyId = 1,
            InvoiceNumber = "INV-TAX",
            CustomerId = 1,
            Status = InvoiceStatus.Sent,
            InvoiceDate = InPeriod,
            TaxAmount = 42m,
            SalesTaxAccountId = 1,
            Total = 542m
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Sales Tax", entry.Source);
        Assert.Equal(0m, entry.Debit);
        Assert.Equal(42m, entry.Credit);
    }

    // ── Source 6: expense categorized to account (DEBIT) ──────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source6_ExpenseCategorizedToAccount_CreatesDebitEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Advertising);
        context.Expenses.Add(new Expense
        {
            Id = 1, CompanyId = 1,
            ExpenseNumber = "EXP-002",
            ExpenseAccountId = 1,    // Advertising account receives the expense
            PaymentAccountId = 999,  // Different account — so Source 2 does not also fire
            Amount = 300m,
            Date = InPeriod,
            PaymentMethod = PaymentMethod.Check
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Expense", entry.Source);
        Assert.Equal(300m, entry.Debit);
        Assert.Equal(0m, entry.Credit);
    }

    // ── Source 7: bill line items to expense account (DEBIT) ──────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source7_BillLineItem_CreatesDebitEntry()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.SuppliesMaterials);
        context.Bills.Add(new Bill
        {
            Id = 10, CompanyId = 1,
            BillNumber = "BILL-001",
            VendorId = 1,
            APAccountId = 999,        // AP account different from test account
            Status = BillStatus.Open,
            BillDate = InPeriod,
            Total = 200m
        });
        context.BillLineItems.Add(new BillLineItem
        {
            Id = 1, CompanyId = 1,
            BillId = 10,
            AccountId = 1,
            Description = "Powder supplies",
            Quantity = 1, UnitPrice = 200m, Amount = 200m
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal("Bill", entry.Source);
        Assert.Equal(200m, entry.Debit);
        Assert.Equal(0m, entry.Credit);
    }

    [Fact]
    public async Task GetAccountLedgerAsync_Source7_DraftAndVoidedBillsExcluded()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.SuppliesMaterials);
        context.Bills.AddRange(
            new Bill { Id = 10, CompanyId = 1, BillNumber = "B-DRAFT", VendorId = 1, APAccountId = 999, Status = BillStatus.Draft,  BillDate = InPeriod, Total = 100m },
            new Bill { Id = 11, CompanyId = 1, BillNumber = "B-VOID",  VendorId = 1, APAccountId = 999, Status = BillStatus.Voided, BillDate = InPeriod, Total = 100m });
        context.BillLineItems.AddRange(
            new BillLineItem { Id = 1, CompanyId = 1, BillId = 10, AccountId = 1, Description = "D", Quantity = 1, UnitPrice = 100m, Amount = 100m },
            new BillLineItem { Id = 2, CompanyId = 1, BillId = 11, AccountId = 1, Description = "V", Quantity = 1, UnitPrice = 100m, Amount = 100m });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Empty(ledger!.Entries);
    }

    // ── Source 8: Accounts Receivable ─────────────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source8_AR_InvoicesDebitPaymentsCredit()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.AccountsReceivable);
        SeedCustomer(context, id: 1);  // Include(i => i.Customer) requires the Customer to exist
        context.Invoices.Add(new Invoice
        {
            Id = 10, CompanyId = 1,
            InvoiceNumber = "INV-AR",
            CustomerId = 1,
            Status = InvoiceStatus.Sent,
            InvoiceDate = InPeriod,
            Total = 400m
        });
        context.Payments.Add(new Payment
        {
            Id = 1, CompanyId = 1,
            InvoiceId = 10,
            Amount = 100m,
            PaymentDate = InPeriod,
            PaymentMethod = PaymentMethod.Cash
            // No DepositAccountId — so Source 1 does not also fire for this account
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Equal(2, ledger!.Entries.Count);
        var invoiceEntry = ledger.Entries.Single(e => e.Source == "Invoice");
        var paymentEntry = ledger.Entries.Single(e => e.Source == "Invoice Payment");
        Assert.Equal(400m, invoiceEntry.Debit);
        Assert.Equal(0m,   invoiceEntry.Credit);
        Assert.Equal(0m,   paymentEntry.Debit);
        Assert.Equal(100m, paymentEntry.Credit);
        Assert.Equal(300m, ledger.ClosingBalance);   // debit-normal: 400 - 100
    }

    // ── Source 9: Accounts Payable ────────────────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_Source9_AP_BillsCreditBillPaymentsDebit()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.AccountsPayable);
        SeedVendor(context, id: 1);    // Include(b => b.Vendor) requires the Vendor to exist
        context.Bills.Add(new Bill
        {
            Id = 10, CompanyId = 1,
            BillNumber = "BILL-AP",
            VendorId = 1,
            APAccountId = 1,           // This is the AP account under test
            Status = BillStatus.Open,
            BillDate = InPeriod,
            Total = 400m
        });
        // Bill must be seeded because the WHERE for BillPayments navigates through Bill.APAccountId
        context.BillPayments.Add(new BillPayment
        {
            Id = 1, CompanyId = 1,
            PaymentNumber = "BP-AP",
            BillId = 10,
            VendorId = 1,
            BankAccountId = 999,       // Different account — so Source 3 does not also fire
            Amount = 150m,
            PaymentDate = InPeriod,
            PaymentMethod = PaymentMethod.Check
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Equal(2, ledger!.Entries.Count);
        var billEntry    = ledger.Entries.Single(e => e.Source == "Bill");
        var paymentEntry = ledger.Entries.Single(e => e.Source == "Bill Payment");
        Assert.Equal(0m,    billEntry.Debit);
        Assert.Equal(400m,  billEntry.Credit);
        Assert.Equal(150m,  paymentEntry.Debit);
        Assert.Equal(0m,    paymentEntry.Credit);
        Assert.Equal(250m,  ledger.ClosingBalance);  // credit-normal: 400 - 150
    }

    // ── Date range filtering ──────────────────────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_DateRangeFiltering_ExcludesEntriesOutsideRange()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Checking);
        var dates = new[]
        {
            new DateTime(2026, 3, 15),   // before period — excluded from entries, goes to priorBalance
            new DateTime(2026, 4, 15),   // in period    — included
            new DateTime(2026, 5, 15),   // after period — excluded
        };
        var i = 0;
        foreach (var date in dates)
        {
            context.Expenses.Add(new Expense
            {
                Id = ++i, CompanyId = 1,
                ExpenseNumber = $"EXP-{i:D3}",
                PaymentAccountId = 1,
                ExpenseAccountId = 999,
                Amount = 100m,
                Date = date,
                PaymentMethod = PaymentMethod.Cash
            });
        }
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var entry = Assert.Single(ledger!.Entries);
        Assert.Equal(InPeriod.Date, entry.Date.Date);
    }

    // ── Running balance ───────────────────────────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_RunningBalance_DebitNormalAccount_CorrectlyComputed()
    {
        await using var context = CreateContext();
        // Opening balance of 100 on Checking (debit-normal); null date means always applied
        SeedAccount(context, id: 1, AccountSubType.Checking, openingBalance: 100m, openingBalanceDate: null);
        SeedInvoice(context, id: 10);  // Include(p => p.Invoice) requires the Invoice to exist
        context.Payments.AddRange(
            new Payment { Id = 1, CompanyId = 1, InvoiceId = 10, DepositAccountId = 1, Amount = 50m,  PaymentDate = new DateTime(2026, 4, 10), PaymentMethod = PaymentMethod.Cash },
            new Payment { Id = 2, CompanyId = 1, InvoiceId = 10, DepositAccountId = 1, Amount = 75m,  PaymentDate = new DateTime(2026, 4, 20), PaymentMethod = PaymentMethod.Cash });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Equal(100m, ledger!.OpeningBalance);
        Assert.Equal(2, ledger.Entries.Count);
        Assert.Equal(150m, ledger.Entries[0].RunningBalance); // 100 + 50
        Assert.Equal(225m, ledger.Entries[1].RunningBalance); // 150 + 75
        Assert.Equal(225m, ledger.ClosingBalance);
    }

    [Fact]
    public async Task GetAccountLedgerAsync_RunningBalance_CreditNormalAccount_CorrectlyComputed()
    {
        await using var context = CreateContext();
        // Opening balance of 200 on Sales (credit-normal); null date means always applied
        SeedAccount(context, id: 1, AccountSubType.Sales, openingBalance: 200m, openingBalanceDate: null);
        // Source 4: InvoiceItems must have the Invoice seeded because the WHERE navigates through Invoice
        context.Invoices.Add(new Invoice
        {
            Id = 10, CompanyId = 1,
            InvoiceNumber = "INV-001",
            CustomerId = 1,
            Status = InvoiceStatus.Sent,
            InvoiceDate = InPeriod,
            Total = 150m
        });
        context.InvoiceItems.AddRange(
            new InvoiceItem { Id = 1, CompanyId = 1, InvoiceId = 10, RevenueAccountId = 1, Description = "A", Quantity = 1, UnitPrice = 100m, TotalPrice = 100m },
            new InvoiceItem { Id = 2, CompanyId = 1, InvoiceId = 10, RevenueAccountId = 1, Description = "B", Quantity = 1, UnitPrice = 50m,  TotalPrice = 50m  });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Equal(200m, ledger!.OpeningBalance);
        Assert.Equal(2, ledger.Entries.Count);
        // Credit-normal: runningBalance += Credit - Debit
        // After entry 1 (Credit=100): 200 + 100 = 300
        // After entry 2 (Credit=50):  300 + 50  = 350
        Assert.Equal(350m, ledger.ClosingBalance);
    }

    // ── Opening balance — future-dated excluded ───────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_FutureDatedOpeningBalance_ExcludedFromPriorBalance()
    {
        await using var context = CreateContext();
        // Opening balance dated 2027-01-01 — later than PeriodEnd (2026-04-30), so excluded
        SeedAccount(context, id: 1, AccountSubType.Checking,
            openingBalance: 500m, openingBalanceDate: new DateTime(2027, 1, 1));
        SeedInvoice(context, id: 10);  // Include(p => p.Invoice) requires the Invoice to exist
        context.Payments.Add(new Payment
        {
            Id = 1, CompanyId = 1,
            InvoiceId = 10, DepositAccountId = 1,
            Amount = 100m, PaymentDate = InPeriod,
            PaymentMethod = PaymentMethod.Cash
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        // OpeningBalance (prior balance) = 0, not 500 — future-dated opening balance excluded
        Assert.Equal(0m, ledger!.OpeningBalance);
        Assert.Equal(100m, ledger.ClosingBalance);   // 0 + 100
    }

    // ── Period totals ─────────────────────────────────────────────────────

    [Fact]
    public async Task GetAccountLedgerAsync_PeriodTotals_CorrectlySumDebitsAndCredits()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.Checking);
        SeedInvoice(context, id: 10);  // Include(p => p.Invoice) requires the Invoice to exist
        // Source 1: payment deposited → DEBIT 200
        context.Payments.Add(new Payment
        {
            Id = 1, CompanyId = 1,
            InvoiceId = 10, DepositAccountId = 1,
            Amount = 200m, PaymentDate = InPeriod,
            PaymentMethod = PaymentMethod.Cash
        });
        // Source 2: expense paid from account → CREDIT 50
        context.Expenses.Add(new Expense
        {
            Id = 1, CompanyId = 1,
            ExpenseNumber = "EXP-001",
            PaymentAccountId = 1,   // Checking pays
            ExpenseAccountId = 999, // Expense account (different) — Source 6 does not also fire
            Amount = 50m,
            Date = InPeriod,
            PaymentMethod = PaymentMethod.Cash
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        Assert.Equal(200m, ledger!.PeriodDebits);
        Assert.Equal(50m,  ledger.PeriodCredits);
        Assert.Equal(150m, ledger.ClosingBalance);  // debit-normal: 200 - 50
    }

    // ── Helpers ───────────────────────────────────────────────────────────

    // ── Cash refund reverses the sale: Sales Returns + Sales Tax debited, AR untouched ──

    [Fact]
    public async Task GetAccountLedgerAsync_CashRefund_ReversesTheSale_DebitsReturnsAndTax_NotAr()
    {
        await using var context = CreateContext();

        context.Accounts.Add(new Account { Id = 1, CompanyId = 1, AccountNumber = "1000", Name = "Checking",          AccountType = AccountType.Asset,     AccountSubType = AccountSubType.Checking,             IsActive = true });
        context.Accounts.Add(new Account { Id = 2, CompanyId = 1, AccountNumber = "1100", Name = "AR",                AccountType = AccountType.Asset,     AccountSubType = AccountSubType.AccountsReceivable,   IsActive = true });
        context.Accounts.Add(new Account { Id = 3, CompanyId = 1, AccountNumber = "2200", Name = "Sales Tax Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsActive = true });
        context.Accounts.Add(new Account { Id = 4, CompanyId = 1, AccountNumber = "4960", Name = "Sales Returns",     AccountType = AccountType.Revenue,   AccountSubType = AccountSubType.OtherIncome,          IsActive = true });

        // Invoice $108 incl. $8 tax, tax posted to account 3.
        context.Invoices.Add(new Invoice
        {
            Id = 99, CompanyId = 1, InvoiceNumber = "INV-0099", CustomerId = 1,
            Status = InvoiceStatus.Sent, InvoiceDate = InPeriod,
            Total = 108m, TaxAmount = 8m, SalesTaxAccountId = 3
        });

        // Full cash refund of $108 paid from Checking.
        context.Refunds.Add(new Refund
        {
            Id = 1, CompanyId = 1, InvoiceId = 99, Amount = 108m,
            RefundDate = InPeriod, RefundMethod = PaymentMethod.Check, DepositAccountId = 1
        });
        await context.SaveChangesAsync();

        var svc = CreateService(context);

        // Sales Returns (4960): revenue portion debited ($100).
        var returns = await svc.GetAccountLedgerAsync(4, PeriodStart, PeriodEnd);
        var returnsEntry = Assert.Single(returns!.Entries, e => e.Source == "Refund");
        Assert.Equal(100m, returnsEntry.Debit);
        Assert.Equal(0m, returnsEntry.Credit);

        // Sales Tax Payable: tax portion debited ($8), relieving the liability.
        var tax = await svc.GetAccountLedgerAsync(3, PeriodStart, PeriodEnd);
        var taxEntry = Assert.Single(tax!.Entries, e => e.Source == "Refund");
        Assert.Equal(8m, taxEntry.Debit);

        // Bank: full $108 credited (cash leaves).
        var bank = await svc.GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);
        var bankEntry = Assert.Single(bank!.Entries, e => e.Source == "Refund");
        Assert.Equal(108m, bankEntry.Credit);

        // AR is no longer touched by refunds under the "reverse the sale" model.
        var ar = await svc.GetAccountLedgerAsync(2, PeriodStart, PeriodEnd);
        Assert.DoesNotContain(ar!.Entries, e => e.Source == "Refund");
    }

    // ── Credit memo: Customer Credits (2350) tracks outstanding store credit, excludes voided ──

    [Fact]
    public async Task GetAccountLedgerAsync_CustomerCredits2350_TracksOutstandingStoreCredit_ExcludesVoided()
    {
        await using var context = CreateContext();

        context.Accounts.Add(new Account { Id = 1, CompanyId = 1, AccountNumber = "2350", Name = "Customer Credits", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsActive = true });
        SeedInvoice(context, id: 99);

        // Active memo: $100 issued, $40 applied → $60 outstanding liability.
        context.CreditMemos.Add(new CreditMemo { Id = 1, CompanyId = 1, MemoNumber = "CM-1", CustomerId = 1, Amount = 100m, AmountApplied = 40m, IssueDate = InPeriod, Status = CreditMemoStatus.PartiallyApplied });
        context.CreditMemoApplications.Add(new CreditMemoApplication { Id = 1, CompanyId = 1, CreditMemoId = 1, InvoiceId = 99, AmountApplied = 40m, AppliedDate = InPeriod });

        // Voided memo: must contribute nothing to the liability.
        context.CreditMemos.Add(new CreditMemo { Id = 2, CompanyId = 1, MemoNumber = "CM-2", CustomerId = 1, Amount = 50m, AmountApplied = 0m, IssueDate = InPeriod, Status = CreditMemoStatus.Voided });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        // CR 100 (issue) + DR 40 (apply) on the active memo; the voided memo is excluded.
        Assert.Equal(2, ledger!.Entries.Count);
        Assert.Equal(60m, ledger.ClosingBalance); // credit-normal liability: 100 issued − 40 applied
    }

    // ── Sales Discounts (4950): recompute mirrors the actual postings so a balance ──
    // ── recalc reproduces the stored balance instead of wiping it to JE-only (O2). ──

    [Fact]
    public async Task GetAccountLedgerAsync_SalesDiscounts4950_IncludesInvoiceDiscountsAndCreditMemoContraRevenue()
    {
        await using var context = CreateContext();

        context.Accounts.Add(new Account { Id = 1, CompanyId = 1, AccountNumber = "4950", Name = "Sales Discounts", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsActive = true });

        // Invoice with a $30 discount → DR 4950 at invoice date.
        context.Invoices.Add(new Invoice
        {
            Id = 99, CompanyId = 1, InvoiceNumber = "INV-0099", CustomerId = 1,
            Status = InvoiceStatus.Sent, InvoiceDate = InPeriod, Total = 270m, DiscountAmount = 30m
        });

        // Active memo $100 → DR 4950 = full amount at issue (the applied portion stays as contra-revenue).
        context.CreditMemos.Add(new CreditMemo { Id = 1, CompanyId = 1, MemoNumber = "CM-1", CustomerId = 1, Amount = 100m, AmountApplied = 40m, IssueDate = InPeriod, Status = CreditMemoStatus.PartiallyApplied });

        // Voided memo $50 with $20 applied → DR 50 at issue, CR (50−20)=30 unapplied remainder at void.
        context.CreditMemos.Add(new CreditMemo { Id = 2, CompanyId = 1, MemoNumber = "CM-2", CustomerId = 1, Amount = 50m, AmountApplied = 20m, IssueDate = InPeriod, Status = CreditMemoStatus.Voided, UpdatedAt = InPeriod });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        // Entries: invoice discount + two memo issuances + one void reversal.
        Assert.Equal(4, ledger!.Entries.Count);
        Assert.Equal(180m, ledger.PeriodDebits);  // 30 + 100 + 50
        Assert.Equal(30m, ledger.PeriodCredits);   // voided unapplied remainder
        // Credit-normal contra-revenue with a net debit balance → −150 (shows in the TB debit column).
        Assert.Equal(-150m, ledger.ClosingBalance);
    }

    // ── Gift-certificate redemptions credit AR (DR 2500 / CR AR) — must appear in the AR ──
    // ── recompute or the trial balance is left out of balance by the redeemed amount (O7). ──

    [Fact]
    public async Task GetAccountLedgerAsync_AR_GiftCertificateRedemption_CreditsAccountsReceivable()
    {
        await using var context = CreateContext();
        SeedAccount(context, id: 1, AccountSubType.AccountsReceivable);
        SeedCustomer(context, id: 1);
        context.Invoices.Add(new Invoice
        {
            Id = 10, CompanyId = 1, InvoiceNumber = "INV-GC", CustomerId = 1,
            Status = InvoiceStatus.Sent, InvoiceDate = InPeriod, Total = 200m
        });
        context.GiftCertificates.Add(new GiftCertificate
        {
            Id = 5, CompanyId = 1, CertificateCode = "GC-5", OriginalAmount = 50m, RedeemedAmount = 50m,
            IssueDate = InPeriod, Status = GiftCertificateStatus.FullyRedeemed
        });
        context.GiftCertificateRedemptions.Add(new GiftCertificateRedemption
        {
            Id = 1, CompanyId = 1, GiftCertificateId = 5, InvoiceId = 10,
            AmountRedeemed = 50m, RedeemedDate = InPeriod
        });
        await context.SaveChangesAsync();

        var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);

        var invoiceEntry = ledger!.Entries.Single(e => e.Source == "Invoice");
        var gcEntry      = ledger.Entries.Single(e => e.Source == "Gift Certificate");
        Assert.Equal(200m, invoiceEntry.Debit);
        Assert.Equal(50m, gcEntry.Credit);
        Assert.Equal(0m, gcEntry.Debit);
        Assert.Equal(150m, ledger.ClosingBalance); // debit-normal AR: 200 invoiced − 50 redeemed
    }

    // ── Inventory consumption posts DR COGS / CR Inventory; both sides must be in the ──
    // ── recompute so a recalc reproduces them and the trial balance stays balanced (O6). ──

    [Fact]
    public async Task GetAccountLedgerAsync_InventoryConsumption_DebitsCogsAndCreditsInventory()
    {
        await using var context = CreateContext();
        // COGS account (id 1) and Inventory asset account (id 2)
        context.Accounts.Add(new Account { Id = 1, CompanyId = 1, AccountNumber = "5100", Name = "Powder & Materials", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsActive = true });
        context.Accounts.Add(new Account { Id = 2, CompanyId = 1, AccountNumber = "1200", Name = "Inventory - Powder", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Inventory, IsActive = true });
        context.InventoryItems.Add(new InventoryItem
        {
            Id = 7, CompanyId = 1, Name = "White Powder", SKU = "WP-1",
            CogsAccountId = 1, InventoryAccountId = 2
        });
        context.InventoryTransactions.Add(new InventoryTransaction
        {
            Id = 1, CompanyId = 1, InventoryItemId = 7,
            TransactionType = InventoryTransactionType.JobUsage,
            Quantity = -4m, UnitCost = 10m, TotalCost = 40m,
            TransactionDate = InPeriod, Reference = "Job 100"
        });
        await context.SaveChangesAsync();

        var svc = CreateService(context);

        var cogs = await svc.GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);
        var cogsEntry = Assert.Single(cogs!.Entries, e => e.Source == "Inventory Usage");
        Assert.Equal(40m, cogsEntry.Debit);
        Assert.Equal(40m, cogs.ClosingBalance); // debit-normal COGS

        var inv = await svc.GetAccountLedgerAsync(2, PeriodStart, PeriodEnd);
        var invEntry = Assert.Single(inv!.Entries, e => e.Source == "Inventory Usage");
        Assert.Equal(40m, invEntry.Credit);
        Assert.Equal(-40m, inv.ClosingBalance); // debit-normal asset relieved by a credit
    }

    private static LedgerService CreateService(ApplicationDbContext context)
        => new LedgerService(context, Mock.Of<ILogger<LedgerService>>());

    private static Account SeedAccount(
        ApplicationDbContext context,
        int id,
        AccountSubType subType,
        int companyId = 1,
        decimal openingBalance = 0m,
        DateTime? openingBalanceDate = null)
    {
        var account = new Account
        {
            Id = id,
            CompanyId = companyId,
            AccountNumber = $"ACCT-{id:D4}",
            Name = $"Account {id}",
            AccountSubType = subType,
            OpeningBalance = openingBalance,
            OpeningBalanceDate = openingBalanceDate,
            IsActive = true
        };
        context.Accounts.Add(account);
        return account;
    }

    private static Invoice SeedInvoice(ApplicationDbContext context, int id, int companyId = 1)
    {
        var invoice = new Invoice
        {
            Id = id,
            CompanyId = companyId,
            InvoiceNumber = $"INV-{id:D4}",
            CustomerId = 1,
            Status = InvoiceStatus.Sent,
            InvoiceDate = InPeriod,
            Total = 0m
        };
        context.Invoices.Add(invoice);
        return invoice;
    }

    private static void SeedVendor(ApplicationDbContext context, int id, int companyId = 1)
    {
        context.Vendors.Add(new Vendor
        {
            Id = id,
            CompanyId = companyId,
            CompanyName = $"Vendor {id}"
        });
    }

    private static void SeedCustomer(ApplicationDbContext context, int id, int companyId = 1)
    {
        context.Customers.Add(new Customer
        {
            Id = id,
            CompanyId = companyId,
            CompanyName = $"Customer {id}"
        });
    }

    private static void SeedBill(ApplicationDbContext context, int id, int companyId = 1)
    {
        context.Bills.Add(new Bill
        {
            Id = id,
            CompanyId = companyId,
            BillNumber = $"BILL-{id:D4}",
            VendorId = 999,
            APAccountId = 999,
            Status = BillStatus.Open,
            BillDate = InPeriod,
            Total = 0m
        });
    }

    private static ApplicationDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        // Use a SuperAdmin user (no CompanyId claim) so that IsPlatformAdmin = true and the
        // global query filter evaluates to !IsDeleted only — not !IsDeleted && CompanyId == null,
        // which would filter out every row. DefaultHttpContext.Session throws, so we mock
        // HttpContext itself and stub Session.TryGetValue to return false (no impersonation).
        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Role, "SuperAdmin") }, "Test");
        var principal = new ClaimsPrincipal(identity);

        byte[]? noBytes = null;
        var sessionMock = new Mock<ISession>();
        sessionMock.Setup(s => s.TryGetValue(It.IsAny<string>(), out noBytes)).Returns(false);

        var httpContextMock = new Mock<HttpContext>();
        httpContextMock.SetupGet(c => c.User).Returns(principal);
        httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object);

        var accessor = new Mock<IHttpContextAccessor>();
        accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object);

        return new ApplicationDbContext(options, accessor.Object, null!);
    }
}
