import { eq } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { getBalance } from './balance.js'
import { debit } from './debit.js'
import { InsufficientBalanceError } from './errors.js'
import { walletBalances } from './schema.js'
import {
  countLedgerRows,
  createTestDb,
  seedSubscriptionBalance,
  seedWalletBalance,
  subscriptions,
} from './test-fixture.js'

const OWNER = 'user-1'
const TENANT = 'tenant-1'

describe('debit + getBalance', () => {
  it('double-spend regression: same idempotency key debits balance exactly once', async () => {
    const db = await createTestDb()
    await seedWalletBalance(db, OWNER, 100n)

    const target = { kind: 'wallet' as const, ownerId: OWNER }
    const debitInput = {
      key: 'debit-idem-1',
      target,
      amount: 30n,
      reason: 'spend',
    }

    await db.transaction((tx) => debit(tx, debitInput))
    await db.transaction((tx) => debit(tx, debitInput))

    expect(await getBalance(db, target)).toBe(70n)
    expect(await countLedgerRows(db)).toBe(1)
  })

  it('insufficient balance throws, rolls back ledger row, leaves balance unchanged', async () => {
    const db = await createTestDb()
    await seedWalletBalance(db, OWNER, 40n)
    const target = { kind: 'wallet' as const, ownerId: OWNER }

    await expect(
      db.transaction((tx) =>
        debit(tx, {
          key: 'debit-fail-1',
          target,
          amount: 50n,
          reason: 'spend',
        }),
      ),
    ).rejects.toBeInstanceOf(InsufficientBalanceError)

    expect(await countLedgerRows(db)).toBe(0)
    expect(await getBalance(db, target)).toBe(40n)
  })

  it('concurrent debits never oversell the starting balance', async () => {
    const db = await createTestDb()
    const starting = 100n
    const debitAmount = 15n
    await seedWalletBalance(db, OWNER, starting)
    const target = { kind: 'wallet' as const, ownerId: OWNER }

    const results = await Promise.all(
      Array.from({ length: 10 }, (_, index) =>
        db
          .transaction((tx) =>
            debit(tx, {
              key: `concurrent-${index}`,
              target,
              amount: debitAmount,
              reason: 'spend',
            }),
          )
          .then(
            (value) => ({ ok: true as const, value }),
            (error) => ({ ok: false as const, error }),
          ),
      ),
    )

    const successes = results.filter((r) => r.ok).length
    const maxSuccesses = Number(starting / debitAmount)
    expect(successes).toBeLessThanOrEqual(maxSuccesses)

    const ending = await getBalance(db, target)
    expect(starting - ending).toBe(BigInt(successes) * debitAmount)
    expect(await countLedgerRows(db)).toBe(successes)
  })

  it('host-column target debits subscriptions.credit_balance correctly', async () => {
    const db = await createTestDb()
    await seedSubscriptionBalance(db, TENANT, 200n)

    const target = {
      kind: 'column' as const,
      table: subscriptions,
      where: eq(subscriptions.tenantId, TENANT),
      balanceColumn: subscriptions.creditBalance,
    }

    const result = await db.transaction((tx) =>
      debit(tx, {
        key: 'sub-debit-1',
        target,
        amount: 75n,
        reason: 'reserve',
      }),
    )

    expect(result).toEqual({ inserted: true, balance: 125n })
    expect(await getBalance(db, target)).toBe(125n)
    expect(await countLedgerRows(db)).toBe(1)
  })
})

describe('getBalance', () => {
  it('reads materialized wallet balance instead of summing ledger deltas', async () => {
    const db = await createTestDb()
    await seedWalletBalance(db, OWNER, 88n)
    const target = { kind: 'wallet' as const, ownerId: OWNER }

    expect(await getBalance(db, target)).toBe(88n)

    await db
      .update(walletBalances)
      .set({ balance: 99n })
      .where(eq(walletBalances.ownerId, OWNER))

    expect(await getBalance(db, target)).toBe(99n)
  })
})
