// @vitest-environment node
/**
 * sweepStuckPayouts — reclaim crash-stuck processing rows.
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { sql, eq } from 'drizzle-orm'
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 { affiliatePayoutsTable } from './schema.js'
import { debitPayoutInTx, sweepStuckPayouts } from './payout.js'

const DAY_MS = 86_400_000

let testDb: TestDb

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

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)
}

async function seedVestingWallet(
  db: TestDb,
  userId: string,
  balances: { maturedMinor: bigint; withdrawableMinor: bigint },
): Promise<void> {
  await db.execute(sql`
    INSERT INTO wallet_vesting (
      owner_id, pending_minor, matured_minor, withdrawable_minor, lifetime_earned_minor
    ) VALUES (
      ${userId}, 0, ${balances.maturedMinor}, ${balances.withdrawableMinor},
      ${balances.maturedMinor + balances.withdrawableMinor}
    )
    ON CONFLICT (owner_id) DO UPDATE SET
      matured_minor = EXCLUDED.matured_minor,
      withdrawable_minor = EXCLUDED.withdrawable_minor,
      lifetime_earned_minor = EXCLUDED.lifetime_earned_minor,
      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, stripe_account_id, stripe_payouts_enabled)
    VALUES (${id}, ${userId}, 'active', 'acct_test', true)
  `)
  return { id }
}

async function createPayoutRow(
  db: TestDb,
  args: {
    userId: string
    enrollmentId: string
    amountAgorot: bigint
    status?: string
    idempotencyKey?: string
  },
): Promise<{ id: string }> {
  const id = crypto.randomUUID()
  const idempotencyKey = args.idempotencyKey ?? `idem-${id}`
  const status = args.status ?? 'approved'
  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}, ${status}, ${idempotencyKey}
    )
  `)
  return { id }
}

async function seedStuckProcessing(
  db: TestDb,
  args: {
    payoutId: string
    userId: string
    amountMinor: bigint
    processingAt: Date
  },
): Promise<void> {
  await db.transaction(async (tx) => {
    await tx
      .update(affiliatePayoutsTable)
      .set({ status: 'processing', processingAt: args.processingAt })
      .where(eq(affiliatePayoutsTable.id, args.payoutId))
    await debitPayoutInTx(tx, args.userId, args.payoutId, args.amountMinor)
  })
}

describe('sweepStuckPayouts — reclaim crash-stuck processing rows', () => {
  beforeAll(async () => {
    testDb = await makeTestDb()
  })

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

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

  it('marks failed + restores debit for a processing row older than the window', async () => {
    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: 2000n,
    })

    const oldProcessingAt = new Date(Date.now() - 11 * 60 * 1000)
    await seedStuckProcessing(testDb, {
      payoutId: payout.id,
      userId,
      amountMinor: 2000n,
      processingAt: oldProcessingAt,
    })

    const result = await sweepStuckPayouts(testDb, 10)
    expect(result.reclaimed).toBe(1)

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

    const payoutRow = await testDb
      .select()
      .from(affiliatePayoutsTable)
      .where(eq(affiliatePayoutsTable.id, payout.id))
    expect(payoutRow[0]!.status).toBe('failed')
    expect(payoutRow[0]!.failureReason).toBe('stuck_processing_sweep')
  })

  it('leaves a fresh processing row (within the window) untouched', async () => {
    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: 1800n,
    })

    const freshProcessingAt = new Date(Date.now() - 2 * 60 * 1000)
    await seedStuckProcessing(testDb, {
      payoutId: payout.id,
      userId,
      amountMinor: 1800n,
      processingAt: freshProcessingAt,
    })

    const result = await sweepStuckPayouts(testDb, 10)
    expect(result.reclaimed).toBe(0)

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

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

  it('status-guard blocks double-restore (row already moved off processing)', async () => {
    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: 2500n,
    })

    const oldProcessingAt = new Date(Date.now() - 15 * 60 * 1000)
    await seedStuckProcessing(testDb, {
      payoutId: payout.id,
      userId,
      amountMinor: 2500n,
      processingAt: oldProcessingAt,
    })

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

    const result = await sweepStuckPayouts(testDb, 10)
    expect(result.reclaimed).toBe(0)

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

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