import { eq, sql } from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import { describe, expect, it } from 'vitest'
import { debitWithRead } from './debit-with-read.js'
import {
  countLedgerRows,
  createTestDb,
  type TestSchema,
  walletBuckets,
} from './test-fixture.js'

const OWNER = 'bucket-owner'

class BucketInsufficientError extends Error {
  override readonly name = 'BucketInsufficientError'
}

async function seedBuckets(
  db: Awaited<ReturnType<typeof createTestDb>>,
  pending: bigint,
  matured: bigint,
): Promise<void> {
  await db.insert(walletBuckets).values({ ownerId: OWNER, pending, matured })
}

describe('debitWithRead', () => {
  it('spends only the matured bucket and leaves pending untouched', async () => {
    const db = await createTestDb()
    await seedBuckets(db, 500n, 200n)

    const result = await db.transaction((tx) =>
      debitWithRead(
        tx,
        {
          key: 'bucket-debit-1',
          reason: 'payout',
          lock: {
            table: walletBuckets,
            where: eq(walletBuckets.ownerId, OWNER),
          },
        },
        ([row]) => {
          const matured = row?.matured as bigint
          const pending = row?.pending as bigint
          const amount = 80n
          if (matured < amount) throw new BucketInsufficientError()

          return {
            delta: -amount,
            apply: async (applyTx: Querier<TestSchema>) => {
              await applyTx
                .update(walletBuckets)
                .set({
                  matured: sql`${walletBuckets.matured} - ${amount}`,
                  pending,
                })
                .where(eq(walletBuckets.ownerId, OWNER))
            },
          }
        },
      ),
    )

    expect(result).toEqual({ inserted: true, id: expect.any(String) })
    const [row] = await db.select().from(walletBuckets).where(eq(walletBuckets.ownerId, OWNER))
    expect(row?.matured).toBe(120n)
    expect(row?.pending).toBe(500n)
    expect(await countLedgerRows(db)).toBe(1)
  })

  it('caller insufficient error leaves buckets and ledger unchanged', async () => {
    const db = await createTestDb()
    await seedBuckets(db, 100n, 40n)

    await expect(
      db.transaction((tx) =>
        debitWithRead(
          tx,
          {
            key: 'bucket-fail-1',
            reason: 'payout',
            lock: {
              table: walletBuckets,
              where: eq(walletBuckets.ownerId, OWNER),
            },
          },
          ([row]) => {
            const matured = row?.matured as bigint
            const amount = 50n
            if (matured < amount) throw new BucketInsufficientError()
            return {
              delta: -amount,
              apply: async () => {},
            }
          },
        ),
      ),
    ).rejects.toBeInstanceOf(BucketInsufficientError)

    const [row] = await db.select().from(walletBuckets).where(eq(walletBuckets.ownerId, OWNER))
    expect(row?.matured).toBe(40n)
    expect(row?.pending).toBe(100n)
    expect(await countLedgerRows(db)).toBe(0)
  })

  it('concurrent debitWithRead calls serialize via FOR UPDATE without lost updates', async () => {
    const db = await createTestDb()
    await seedBuckets(db, 0n, 100n)
    const amount = 30n

    const results = await Promise.all([
      db.transaction((tx) =>
        debitWithRead(
          tx,
          {
            key: 'serial-a',
            reason: 'payout',
            lock: {
              table: walletBuckets,
              where: eq(walletBuckets.ownerId, OWNER),
            },
          },
          ([row]) => {
            const matured = row?.matured as bigint
            if (matured < amount) throw new BucketInsufficientError()
            return {
              delta: -amount,
              apply: async (applyTx: Querier<TestSchema>) => {
                await applyTx
                  .update(walletBuckets)
                  .set({ matured: sql`${walletBuckets.matured} - ${amount}` })
                  .where(eq(walletBuckets.ownerId, OWNER))
              },
            }
          },
        ),
      ),
      db.transaction((tx) =>
        debitWithRead(
          tx,
          {
            key: 'serial-b',
            reason: 'payout',
            lock: {
              table: walletBuckets,
              where: eq(walletBuckets.ownerId, OWNER),
            },
          },
          ([row]) => {
            const matured = row?.matured as bigint
            if (matured < amount) throw new BucketInsufficientError()
            return {
              delta: -amount,
              apply: async (applyTx: Querier<TestSchema>) => {
                await applyTx
                  .update(walletBuckets)
                  .set({ matured: sql`${walletBuckets.matured} - ${amount}` })
                  .where(eq(walletBuckets.ownerId, OWNER))
              },
            }
          },
        ),
      ),
      db.transaction((tx) =>
        debitWithRead(
          tx,
          {
            key: 'serial-c',
            reason: 'payout',
            lock: {
              table: walletBuckets,
              where: eq(walletBuckets.ownerId, OWNER),
            },
          },
          ([row]) => {
            const matured = row?.matured as bigint
            if (matured < amount) throw new BucketInsufficientError()
            return {
              delta: -amount,
              apply: async (applyTx: Querier<TestSchema>) => {
                await applyTx
                  .update(walletBuckets)
                  .set({ matured: sql`${walletBuckets.matured} - ${amount}` })
                  .where(eq(walletBuckets.ownerId, OWNER))
              },
            }
          },
        ),
      ),
    ])

    const inserted = results.filter((r) => r.inserted).length
    expect(inserted).toBe(3)

    const [row] = await db.select().from(walletBuckets).where(eq(walletBuckets.ownerId, OWNER))
    expect(row?.matured).toBe(10n)
    expect(await countLedgerRows(db)).toBe(3)
  })

  it('idempotent key replay does not double-apply bucket mutation', async () => {
    const db = await createTestDb()
    await seedBuckets(db, 0n, 150n)
    const amount = 60n
    const input = {
      key: 'bucket-idem-1',
      reason: 'payout',
      lock: {
        table: walletBuckets,
        where: eq(walletBuckets.ownerId, OWNER),
      },
    }

    const plan = ([row]: Array<Record<string, unknown>>) => {
      const matured = row?.matured as bigint
      if (matured < amount) throw new BucketInsufficientError()
      return {
        delta: -amount,
        apply: async (applyTx: Querier<TestSchema>) => {
          await applyTx
            .update(walletBuckets)
            .set({ matured: sql`${walletBuckets.matured} - ${amount}` })
            .where(eq(walletBuckets.ownerId, OWNER))
        },
      }
    }

    await db.transaction((tx) => debitWithRead(tx, input, plan))
    await db.transaction((tx) => debitWithRead(tx, input, plan))

    const [row] = await db.select().from(walletBuckets).where(eq(walletBuckets.ownerId, OWNER))
    expect(row?.matured).toBe(90n)
    expect(await countLedgerRows(db)).toBe(1)
  })
})
