import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
  recordInvoicePayment,
  reverseInvoicePaymentTx,
} from '../../src/queries/invoice-payments'

function makeRecordPaymentDb(existingInvoice: {
  id: string
  tenantId: string
  status: string
  amountPaid: string
  total: string
  currency: string
  paidAt: Date | null
}) {
  const insertedValues: unknown[] = []
  const updatedValues: unknown[] = []
  const auditValues: unknown[] = []
  let insertCall = 0
  const paymentRow = {
    id: 'pay-1',
    invoiceId: existingInvoice.id,
    amount: '120.00',
    currency: existingInvoice.currency,
    paidAt: new Date('2026-07-03T00:00:00.000Z'),
    source: 'manual',
    reference: 'BANK-123',
    recordedBy: 'user-1',
    note: 'rounded up',
    receiptId: null,
    createdAt: new Date('2026-07-03T00:00:00.000Z'),
  }

  const tx = {
    select: vi.fn(() => ({
      from: vi.fn(() => ({
        where: vi.fn(() => ({
          limit: vi.fn(() => ({
            for: vi.fn(async () => [existingInvoice]),
          })),
        })),
      })),
    })),
    insert: vi.fn(() => ({
      values: vi.fn((value: unknown) => {
        insertCall += 1
        if (insertCall === 1) {
          insertedValues.push(value)
          return {
            returning: async () => [paymentRow],
          }
        }
        auditValues.push(value)
        return Promise.resolve(undefined)
      }),
    })),
    update: vi.fn(() => ({
      set: vi.fn((value: unknown) => {
        updatedValues.push(value)
        return {
          where: async () => undefined,
        }
      }),
    })),
  }

  return {
    insertedValues,
    updatedValues,
    auditValues,
    db: {
      transaction: async <T>(fn: (innerTx: typeof tx) => Promise<T>) => fn(tx),
    },
  }
}

function makeReversePaymentTx(args: {
  invoice: {
    id: string
    tenantId: string
    status: string
    total: string
    amountPaid: string
    paidAt: Date | null
    sentAt: Date | null
    approvedAt: Date | null
    taxIssuedAt: Date | null
  }
  payment: {
    id: string
    invoiceId: string
    tenantId: string
    amount: string
    receiptId: string | null
  }
  sumTotal: string | null
}) {
  const updateCalls: unknown[] = []
  const auditValues: unknown[] = []
  let selectCall = 0

  return {
    updateCalls,
    auditValues,
    tx: {
      select: vi.fn(() => {
        const nextSelect = ++selectCall
        if (nextSelect <= 2) {
          return {
            from: vi.fn(() => ({
              where: vi.fn(() => ({
                limit: vi.fn(() =>
                  nextSelect === 1
                    ? {
                        for: vi.fn(async () => [args.invoice]),
                      }
                    : Promise.resolve([args.payment]),
                ),
              })),
            })),
          }
        }
        return {
          from: vi.fn(() => ({
            where: async () => [{ total: args.sumTotal }],
          })),
        }
      }),
      delete: vi.fn(() => ({
        where: async () => undefined,
      })),
      update: vi.fn(() => ({
        set: vi.fn((value: unknown) => {
          updateCalls.push(value)
          return {
            where: async () => undefined,
          }
        }),
      })),
      insert: vi.fn(() => ({
        values: async (value: unknown) => {
          auditValues.push(value)
          return undefined
        },
      })),
    },
  }
}

describe('invoice payment queries', () => {
  beforeEach(() => {
    vi.useRealTimers()
  })

  it('accepts manual overpayments and records the overpayment amount', async () => {
    const { db, insertedValues, updatedValues } = makeRecordPaymentDb({
      id: 'inv-1',
      tenantId: 'tenant-1',
      status: 'TAX_ISSUED',
      amountPaid: '90.00',
      total: '100.00',
      currency: 'ILS',
      paidAt: null,
    })

    const result = await recordInvoicePayment(
      db as never,
      'tenant-1',
      'inv-1',
      'user-1',
      {
        amount: 30,
        paidAt: '2026-07-03T00:00:00.000Z',
        source: 'manual',
        reference: 'BANK-123',
        note: 'rounded up',
      },
    )

    expect(result.amountPaid).toBe(120)
    expect(result.balance).toBe(0)
    expect(result.overpaymentAmount).toBe(20)
    expect(insertedValues).toHaveLength(1)
    expect(updatedValues).toHaveLength(1)
  })

  it('settles credit notes against the absolute credited amount', async () => {
    const { db, updatedValues } = makeRecordPaymentDb({
      id: 'cn-1',
      tenantId: 'tenant-1',
      status: 'TAX_ISSUED',
      amountPaid: '20.00',
      total: '-100.00',
      currency: 'ILS',
      paidAt: null,
    })

    const result = await recordInvoicePayment(
      db as never,
      'tenant-1',
      'cn-1',
      'user-1',
      {
        amount: 40,
        paidAt: '2026-07-03T00:00:00.000Z',
        source: 'manual',
      },
    )

    expect(result.amountPaid).toBe(60)
    expect(result.balance).toBe(40)
    expect(result.overpaymentAmount).toBe(0)
    expect(updatedValues[0]).toMatchObject({
      amountPaid: '60',
      overpaymentAmount: '0',
      status: 'PARTIALLY_PAID',
      paidAt: null,
    })
  })

  it('restores SENT when the last payment is reversed from a sent invoice path', async () => {
    const base = makeReversePaymentTx({
      invoice: {
        id: 'inv-1',
        tenantId: 'tenant-1',
        status: 'PARTIALLY_PAID',
        total: '100.00',
        amountPaid: '40.00',
        paidAt: null,
        sentAt: new Date('2026-06-01T00:00:00.000Z'),
        approvedAt: null,
        taxIssuedAt: null,
      },
      payment: {
        id: 'pay-1',
        invoiceId: 'inv-1',
        tenantId: 'tenant-1',
        amount: '40.00',
        receiptId: null,
      },
      sumTotal: null,
    })

    await reverseInvoicePaymentTx(
      base.tx as never,
      'tenant-1',
      'inv-1',
      'pay-1',
      'user-1',
    )

    expect(base.updateCalls).toHaveLength(1)
    expect(base.updateCalls[0]).toMatchObject({
      amountPaid: '0',
      overpaymentAmount: '0',
      status: 'SENT',
      paidAt: null,
    })
    expect(base.auditValues).toHaveLength(1)
  })
})
