// @vitest-environment node
/**
 * Payout debit/restore integration oracle — core debitWithRead on wallet_vesting.
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { sql, eq, and } from 'drizzle-orm'
import { ledgerEntries, walletBalances } from '@platform-modules/ledger'
import { walletVesting } from '@platform-modules/ledger/vesting'
import { makeTestDb, closeTestDb, resetTables, type TestDb } from './test/pglite-db.js'
import { createUser } from './test/fixtures.js'
import {
  affiliateEntriesTable,
  affiliatePayoutsTable,
} from './schema.js'
import { isInsufficientBalanceError, isInvalidPayoutAmountError } from './errors.js'

let testDb: TestDb

const DAY_MS = 86_400_000

const getPayout = () => import('./payout.js')

async function seedReferralSettings(db: TestDb): Promise<void> {
  await db.execute(sql`
    INSERT INTO referral_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING
  `)
}

/** Born-matured eligible credit — recomputeWithdrawable needs ledger-backed eligibleMinor. */
async function seedEligibleMaturedCredit(
  db: TestDb,
  userId: string,
  amountMinor: bigint,
): Promise<void> {
  await seedReferralSettings(db)
  const { accrueCommission } = await import('./accrual.js')
  const sourceId = crypto.randomUUID()
  const pastMature = new Date(Date.now() - DAY_MS)

  await db.transaction((tx) =>
    accrueCommission(tx, {
      userId,
      amountMinor,
      entryType: 'affiliate_commission',
      sourceType: 'purchase',
      sourceId,
      matureAt: pastMature,
    }),
  )

  const entryResult = (await db.execute(sql`
    SELECT entry_id FROM affiliate_entries
    WHERE owner_id = ${userId} AND source_id = ${sourceId}
    LIMIT 1
  `)) as { rows: Array<{ entry_id: string }> }
  const entryId = entryResult.rows[0]!.entry_id

  await db.execute(sql`
    UPDATE ledger_entry_vesting
    SET withdrawable_at = ${pastMature.toISOString()}, swept_at = NOW()
    WHERE entry_id = ${entryId}
  `)
}

async function syncWithdrawable(db: TestDb, userId: string): Promise<void> {
  const { recomputeWithdrawable } = await import('@platform-modules/ledger/vesting')
  const { computeOwnerEligibleAndPaid } = await import('./maturity-sweep.js')
  await db.transaction(async (tx) => {
    await recomputeWithdrawable(tx, { ownerId: userId }, async () => {
      const { eligibleMinor, paidMinor } = await computeOwnerEligibleAndPaid(tx, userId)
      return { eligibleMinor, paidMinor }
    })
  })
}

async function seedOwnerCreditForPayout(
  db: TestDb,
  userId: string,
  maturedMinor: bigint,
): Promise<void> {
  await seedEligibleMaturedCredit(db, userId, maturedMinor)
}

/** Manual bucket seed for inconsistent-state gate tests only — skips recompute. */
async function seedVestingWallet(
  db: TestDb,
  userId: string,
  balances: {
    pendingMinor?: bigint
    maturedMinor?: bigint
    withdrawableMinor?: bigint
  },
): Promise<void> {
  const pending = balances.pendingMinor ?? 0n
  const matured = balances.maturedMinor ?? 0n
  const withdrawable = balances.withdrawableMinor ?? 0n

  await db.execute(sql`
    INSERT INTO wallet_vesting (
      owner_id, pending_minor, matured_minor, withdrawable_minor, lifetime_earned_minor
    ) VALUES (${userId}, ${pending}, ${matured}, ${withdrawable}, ${pending + matured + withdrawable})
    ON CONFLICT (owner_id) DO UPDATE SET
      pending_minor = EXCLUDED.pending_minor,
      matured_minor = EXCLUDED.matured_minor,
      withdrawable_minor = EXCLUDED.withdrawable_minor,
      lifetime_earned_minor = EXCLUDED.lifetime_earned_minor,
      updated_at = NOW()
  `)
}

async function seedCommerceBalance(db: TestDb, ownerId: string, balance: bigint): Promise<void> {
  await db.execute(sql`
    INSERT INTO wallet_balances (owner_id, balance)
    VALUES (${ownerId}, ${balance})
    ON CONFLICT (owner_id) DO UPDATE SET balance = EXCLUDED.balance, updated_at = NOW()
  `)
}

async function createEnrollment(db: TestDb, userId: string): Promise<{ id: string }> {
  const id = crypto.randomUUID()
  await db.execute(sql`
    INSERT INTO affiliate_enrollments (id, user_id, status)
    VALUES (${id}, ${userId}, 'active')
  `)
  return { id }
}

async function createPayoutRow(
  db: TestDb,
  args: { userId: string; enrollmentId: string; amountAgorot: bigint; idempotencyKey?: string },
): Promise<{ id: string }> {
  const id = crypto.randomUUID()
  const idempotencyKey = args.idempotencyKey ?? `idem-${id}`
  await db.execute(sql`
    INSERT INTO affiliate_payouts (
      id, user_id, enrollment_id, amount_agorot, status, idempotency_key
    ) VALUES (
      ${id}, ${args.userId}, ${args.enrollmentId}, ${args.amountAgorot}, 'approved', ${idempotencyKey}
    )
  `)
  return { id }
}

describe('payout debit/restore integration', () => {
  beforeAll(async () => {
    testDb = await makeTestDb()
  })

  afterAll(async () => {
    await closeTestDb()
  })

  beforeEach(async () => {
    await resetTables(testDb)
  })

  describe('debitPayoutInTx', () => {
    it('rejects when withdrawableMinor exceeds maturedMinor — matured ceiling is sole runtime defense', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 3000n,
      })

      await seedVestingWallet(testDb, userId, {
        maturedMinor: 2000n,
        withdrawableMinor: 5000n,
      })

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 3000n)),
      ).rejects.toSatisfy((e: unknown) => {
        if (!isInsufficientBalanceError(e)) return false
        expect(e.detail?.bucket).toBe('matured')
        expect(e.detail?.requiredMinor).toBe(3000n)
        expect(e.detail?.availableMinor).toBe(2000n)
        return true
      })

      const ledgerCount = await testDb.select().from(ledgerEntries)
      expect(ledgerCount).toHaveLength(0)
    })

    it('happy path — decrements maturedMinor, recomputes withdrawableMinor, writes side-row + FK', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 1000n,
      })
      await seedCommerceBalance(testDb, userId, 99_000n)

      const ledgerEntryId = await testDb.transaction((tx) =>
        debitPayoutInTx(tx, userId, payout.id, 1000n),
      )

      expect(typeof ledgerEntryId).toBe('string')
      expect(ledgerEntryId.length).toBeGreaterThan(0)

      const ledger = await testDb
        .select()
        .from(ledgerEntries)
        .where(eq(ledgerEntries.id, ledgerEntryId))
      expect(ledger).toHaveLength(1)
      expect(ledger[0]!.delta).toBe(-1000n)

      const sideRow = await testDb
        .select()
        .from(affiliateEntriesTable)
        .where(eq(affiliateEntriesTable.entryId, ledgerEntryId))
      expect(sideRow).toHaveLength(1)
      expect(sideRow[0]!.entryType).toBe('redemption')
      expect(sideRow[0]!.sourceType).toBe('affiliate_payout')
      expect(sideRow[0]!.sourceId).toBe(payout.id)
      expect(sideRow[0]!.ownerId).toBe(userId)

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.withdrawableMinor).toBe(4000n)
      expect(vesting[0]!.maturedMinor).toBe(4000n)

      const commerce = await testDb
        .select()
        .from(walletBalances)
        .where(eq(walletBalances.ownerId, userId))
      expect(commerce[0]!.balance).toBe(99_000n)

      const payoutRow = await testDb
        .select()
        .from(affiliatePayoutsTable)
        .where(eq(affiliatePayoutsTable.id, payout.id))
      expect(payoutRow[0]!.ledgerEntryId).toBe(ledgerEntryId)
    })

    it('succeeds for pure-affiliate owner with no walletBalances row', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 2000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 500n,
      })

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 500n))

      const commerce = await testDb
        .select()
        .from(walletBalances)
        .where(eq(walletBalances.ownerId, userId))
      expect(commerce).toHaveLength(0)

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.withdrawableMinor).toBe(1500n)
      expect(vesting[0]!.maturedMinor).toBe(1500n)
    })

    it('throws InsufficientBalanceError when withdrawable bucket is short', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 4000n,
      })
      await syncWithdrawable(testDb, userId)

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 2000n)),
      ).rejects.toSatisfy((e: unknown) => {
        if (!isInsufficientBalanceError(e)) return false
        expect(e.detail?.bucket).toBe('withdrawable')
        expect(e.detail?.requiredMinor).toBe(2000n)
        expect(e.detail?.availableMinor).toBe(1000n)
        return true
      })

      const redemptionRows = await testDb
        .select()
        .from(ledgerEntries)
        .where(eq(ledgerEntries.reason, 'redemption'))
      expect(redemptionRows).toHaveLength(0)
    })

    it('throws InsufficientBalanceError when matured bucket is short (withdrawable sufficient)', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 3000n,
      })

      await seedVestingWallet(testDb, userId, {
        maturedMinor: 2000n,
        withdrawableMinor: 5000n,
      })

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 3000n)),
      ).rejects.toSatisfy((e: unknown) => {
        if (!isInsufficientBalanceError(e)) return false
        expect(e.detail?.bucket).toBe('matured')
        expect(e.detail?.requiredMinor).toBe(3000n)
        expect(e.detail?.availableMinor).toBe(2000n)
        return true
      })
    })

    it('throws InsufficientBalanceError when withdrawable is zero', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 500n,
      })
      await testDb.execute(sql`
        UPDATE wallet_vesting SET withdrawable_minor = 0
        WHERE owner_id = ${userId}
      `)

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 500n)),
      ).rejects.toSatisfy((e: unknown) => {
        if (!isInsufficientBalanceError(e)) return false
        expect(e.detail?.bucket).toBe('withdrawable')
        expect(e.detail?.availableMinor).toBe(0n)
        return true
      })
    })

    it('idempotency — second debit does not double-debit balance', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 800n,
      })

      const first = await testDb.transaction((tx) =>
        debitPayoutInTx(tx, userId, payout.id, 800n),
      )
      const second = await testDb.transaction((tx) =>
        debitPayoutInTx(tx, userId, payout.id, 800n),
      )

      expect(first).toBe(second)

      const ledgerRows = await testDb
        .select()
        .from(ledgerEntries)
        .where(eq(ledgerEntries.idempotencyKey, `redemption:affiliate_payout:${payout.id}`))
      expect(ledgerRows).toHaveLength(1)

      const sideRows = await testDb
        .select()
        .from(affiliateEntriesTable)
        .where(eq(affiliateEntriesTable.sourceId, payout.id))
      expect(sideRows).toHaveLength(1)

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.withdrawableMinor).toBe(4200n)
      expect(vesting[0]!.maturedMinor).toBe(4200n)
    })

    it('blocks a second payout when maturedMinor was depleted by the first debit', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      const amount = 5000n
      await seedOwnerCreditForPayout(testDb, userId, amount)
      await syncWithdrawable(testDb, userId)
      const payout1 = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: amount,
      })

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout1.id, amount))

      await testDb
        .update(affiliatePayoutsTable)
        .set({ status: 'paid', paidAt: new Date() })
        .where(eq(affiliatePayoutsTable.id, payout1.id))

      const payout2 = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: amount,
      })

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout2.id, amount)),
      ).rejects.toSatisfy((e: unknown) => isInsufficientBalanceError(e))

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.maturedMinor).toBe(0n)
    })

    it('canonical builder output is byte-identical to the pre-convergence literal key', async () => {
      const { buildIdempotencyKey } = await import('./accrual.js')
      const id = crypto.randomUUID()
      expect(buildIdempotencyKey('redemption', 'affiliate_payout', id)).toBe(
        `redemption:affiliate_payout:${id}`,
      )
    })

    it('rejects a negative amount — a negative "debit" must not credit the wallet', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 1000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: -500n,
      })

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, -500n)),
      ).rejects.toSatisfy((e: unknown) => isInvalidPayoutAmountError(e))

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.maturedMinor).toBe(1000n)

      const ledgerRows = await testDb
        .select()
        .from(ledgerEntries)
        .where(eq(ledgerEntries.idempotencyKey, `redemption:affiliate_payout:${payout.id}`))
      expect(ledgerRows).toHaveLength(0)
    })

    it('rejects a zero amount', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 1000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 0n,
      })

      await expect(
        testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 0n)),
      ).rejects.toSatisfy((e: unknown) => isInvalidPayoutAmountError(e))
    })

    it('wallet_balances.balance is unchanged across a payout debit', async () => {
      const { debitPayoutInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 8000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 2500n,
      })
      await seedCommerceBalance(testDb, userId, 42_000n)

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 2500n))

      const commerce = await testDb
        .select()
        .from(walletBalances)
        .where(eq(walletBalances.ownerId, userId))
      expect(commerce[0]!.balance).toBe(42_000n)
    })
  })

  describe('restorePayoutDebitInTx', () => {
    it('restores maturedMinor + recomputed withdrawable after payout status is released', async () => {
      const { debitPayoutInTx, restorePayoutDebitInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 1500n,
      })

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 1500n))

      await testDb
        .update(affiliatePayoutsTable)
        .set({ status: 'failed', failedAt: new Date() })
        .where(eq(affiliatePayoutsTable.id, payout.id))

      const restored = await testDb.transaction((tx) =>
        restorePayoutDebitInTx(tx, userId, payout.id, 1500n),
      )
      expect(restored).toBe(true)

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.maturedMinor).toBe(5000n)
      expect(vesting[0]!.withdrawableMinor).toBe(5000n)

      const reversal = await testDb
        .select()
        .from(affiliateEntriesTable)
        .where(eq(affiliateEntriesTable.sourceId, payout.id))
      expect(reversal).toHaveLength(2)
      const adjustment = reversal.find((r) => r.entryType === 'adjustment')
      expect(adjustment?.sourceType).toBe('affiliate_payout_reversal')

      const reversalLedger = await testDb
        .select()
        .from(ledgerEntries)
        .where(eq(ledgerEntries.idempotencyKey, `adjustment:affiliate_payout_reversal:${payout.id}`))
      expect(reversalLedger).toHaveLength(1)
      expect(reversalLedger[0]!.delta).toBe(1500n)
    })

    it('restore-no-double-credit — eager matured restore without released status would over-credit', async () => {
      const { debitPayoutInTx, restorePayoutDebitInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 6000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 2000n,
      })

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 2000n))

      const afterDebit = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(afterDebit[0]!.maturedMinor).toBe(4000n)
      expect(afterDebit[0]!.withdrawableMinor).toBe(4000n)

      await testDb
        .update(affiliatePayoutsTable)
        .set({ status: 'failed', failedAt: new Date() })
        .where(eq(affiliatePayoutsTable.id, payout.id))

      await testDb.transaction((tx) =>
        restorePayoutDebitInTx(tx, userId, payout.id, 2000n),
      )

      const afterRestore = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(afterRestore[0]!.maturedMinor).toBe(6000n)
      expect(afterRestore[0]!.withdrawableMinor).toBe(6000n)
    })

    it('rejects a non-positive amount — a negative "restore" must not debit the wallet', async () => {
      const { debitPayoutInTx, restorePayoutDebitInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 1500n,
      })

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 1500n))

      await testDb
        .update(affiliatePayoutsTable)
        .set({ status: 'failed', failedAt: new Date() })
        .where(eq(affiliatePayoutsTable.id, payout.id))

      await expect(
        testDb.transaction((tx) => restorePayoutDebitInTx(tx, userId, payout.id, -1500n)),
      ).rejects.toSatisfy((e: unknown) => isInvalidPayoutAmountError(e))

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.maturedMinor).toBe(3500n)

      const reversalLedger = await testDb
        .select()
        .from(ledgerEntries)
        .where(
          eq(ledgerEntries.idempotencyKey, `adjustment:affiliate_payout_reversal:${payout.id}`),
        )
      expect(reversalLedger).toHaveLength(0)
    })

    it('idempotency — second restore is a no-op', async () => {
      const { debitPayoutInTx, restorePayoutDebitInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 3000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 600n,
      })

      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 600n))

      await testDb
        .update(affiliatePayoutsTable)
        .set({ status: 'failed', failedAt: new Date() })
        .where(eq(affiliatePayoutsTable.id, payout.id))

      const first = await testDb.transaction((tx) =>
        restorePayoutDebitInTx(tx, userId, payout.id, 600n),
      )
      const second = await testDb.transaction((tx) =>
        restorePayoutDebitInTx(tx, userId, payout.id, 600n),
      )

      expect(first).toBe(true)
      expect(second).toBe(false)

      const adjustments = await testDb
        .select()
        .from(affiliateEntriesTable)
        .where(eq(affiliateEntriesTable.sourceType, 'affiliate_payout_reversal'))
      expect(adjustments).toHaveLength(1)

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.withdrawableMinor).toBe(3000n)
    })

    it('cross-writer reversal — restore dedups against a pre-existing CANONICAL reversal (no double-credit, no crash)', async () => {
      const { debitPayoutInTx, restorePayoutDebitInTx } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 4000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 2000n,
      })

      // Debit the payout (matured 4000 -> 2000).
      await testDb.transaction((tx) => debitPayoutInTx(tx, userId, payout.id, 2000n))
      const afterDebit = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(afterDebit[0]!.maturedMinor).toBe(2000n)

      // Simulate ANOTHER writer (W1 fold / host bridge) reversing this payout's debit FIRST:
      // it writes the CANONICAL reversal ledger row + the affiliate_entries triple AND credits the wallet (matured 2000 -> 4000).
      const canonicalKey = `adjustment:affiliate_payout_reversal:${payout.id}`
      const priorLedgerId = crypto.randomUUID()
      const reversalAmount = 2000n
      await testDb.insert(ledgerEntries).values({
        id: priorLedgerId,
        delta: reversalAmount,
        reason: 'adjustment',
        ref: { sourceType: 'affiliate_payout_reversal', sourceId: payout.id, userId },
        idempotencyKey: canonicalKey,
      })
      await testDb.insert(affiliateEntriesTable).values({
        entryId: priorLedgerId,
        ownerId: userId,
        entryType: 'adjustment',
        sourceType: 'affiliate_payout_reversal',
        sourceId: payout.id,
        memo: `fold_reversal:${payout.id}`,
      })
      await testDb
        .update(walletVesting)
        .set({
          maturedMinor: sql`${walletVesting.maturedMinor} + ${reversalAmount}`,
          updatedAt: new Date(),
        })
        .where(eq(walletVesting.ownerId, userId))
      const beforeModuleRestore = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(beforeModuleRestore[0]!.maturedMinor).toBe(4000n)

      // The MODULE restore now fires on the SAME payout. It must dedup on the shared canonical key,
      // return false, and credit NOTHING — no UNIQUE crash, no double-credit, no duplicate rows.
      const result = await testDb.transaction((tx) =>
        restorePayoutDebitInTx(tx, userId, payout.id, 2000n),
      )
      expect(result).toBe(false)

      // Money conserved: matured stays 4000 (NOT 6000) — the prior writer's credit is not duplicated.
      const afterModuleRestore = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(afterModuleRestore[0]!.maturedMinor).toBe(4000n)
      expect(afterModuleRestore[0]!.maturedMinor).toBe(beforeModuleRestore[0]!.maturedMinor)

      // Exactly one reversal ledger row + one triple — no duplicate inserted.
      const ledgerRows = await testDb
        .select()
        .from(ledgerEntries)
        .where(eq(ledgerEntries.idempotencyKey, canonicalKey))
      expect(ledgerRows).toHaveLength(1)

      const triple = await testDb
        .select()
        .from(affiliateEntriesTable)
        .where(
          and(
            eq(affiliateEntriesTable.sourceType, 'affiliate_payout_reversal'),
            eq(affiliateEntriesTable.sourceId, payout.id),
          ),
        )
      expect(triple).toHaveLength(1)
    })
  })

  describe('ensurePayoutLedgerDebit', () => {
    it('returns existing ledgerEntryId without re-debiting', async () => {
      const { debitPayoutInTx, ensurePayoutLedgerDebit } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 4000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 700n,
      })

      const ledgerEntryId = await testDb.transaction((tx) =>
        debitPayoutInTx(tx, userId, payout.id, 700n),
      )

      const again = await testDb.transaction((tx) =>
        ensurePayoutLedgerDebit(tx, {
          id: payout.id,
          userId,
          amountAgorot: 700n,
          ledgerEntryId,
        }),
      )

      expect(again).toBe(ledgerEntryId)

      const vesting = await testDb
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
      expect(vesting[0]!.withdrawableMinor).toBe(3300n)
      expect(vesting[0]!.maturedMinor).toBe(3300n)
    })
  })

  describe('verifyPayoutReadyForSettlement', () => {
    it('rejects entryType/sourceType mismatch on the affiliate_entries side-row', async () => {
      const { debitPayoutInTx, verifyPayoutReadyForSettlement, PayoutLedgerMismatchError } =
        await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      await seedOwnerCreditForPayout(testDb, userId, 5000n)
      await syncWithdrawable(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 1000n,
      })

      const ledgerEntryId = await testDb.transaction((tx) =>
        debitPayoutInTx(tx, userId, payout.id, 1000n),
      )

      await testDb
        .update(affiliateEntriesTable)
        .set({ sourceType: 'tampered_source' })
        .where(eq(affiliateEntriesTable.entryId, ledgerEntryId))

      await expect(
        testDb.transaction((tx) =>
          verifyPayoutReadyForSettlement(tx, {
            id: payout.id,
            userId,
            amountAgorot: 1000n,
            ledgerEntryId,
          }),
        ),
      ).rejects.toBeInstanceOf(PayoutLedgerMismatchError)
    })

    it('rejects when ledger core delta does not match payout amount — bigint amount tamper', async () => {
      const { verifyPayoutReadyForSettlement, PayoutLedgerMismatchError } = await getPayout()
      const { id: userId } = await createUser(testDb)
      const enrollment = await createEnrollment(testDb, userId)
      const payout = await createPayoutRow(testDb, {
        userId,
        enrollmentId: enrollment.id,
        amountAgorot: 1000n,
      })

      const entryId = crypto.randomUUID()
      await testDb.execute(sql`
        INSERT INTO ledger_entries (id, delta, reason, idempotency_key)
        VALUES (${entryId}, ${-2000}, 'redemption', ${`redemption:affiliate_payout:${payout.id}`})
      `)
      await testDb.insert(affiliateEntriesTable).values({
        entryId,
        ownerId: userId,
        entryType: 'redemption',
        sourceType: 'affiliate_payout',
        sourceId: payout.id,
        memo: payout.id,
      })
      await testDb
        .update(affiliatePayoutsTable)
        .set({ ledgerEntryId: entryId })
        .where(eq(affiliatePayoutsTable.id, payout.id))

      await expect(
        testDb.transaction((tx) =>
          verifyPayoutReadyForSettlement(tx, {
            id: payout.id,
            userId,
            amountAgorot: 1000n,
            ledgerEntryId: entryId,
          }),
        ),
      ).rejects.toBeInstanceOf(PayoutLedgerMismatchError)
    })
  })
})
