// @vitest-environment node
/**
 * settlePayout — claim tx → executor OUTSIDE tx → persist/restore.
 */
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 { settlePayout, sweepStuckPayouts, type PayoutExecutor } 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,
  opts?: { stripeAccountId?: string | null },
): Promise<{ id: string }> {
  const id = crypto.randomUUID()
  const stripeAccountId = opts && 'stripeAccountId' in opts ? opts.stripeAccountId : 'acct_test'
  await db.execute(sql`
    INSERT INTO affiliate_enrollments (id, user_id, status, stripe_account_id, stripe_payouts_enabled)
    VALUES (${id}, ${userId}, 'active', ${stripeAccountId}, 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 }
}

describe('settlePayout — claim → execute OUTSIDE tx → persist/restore', () => {
  beforeAll(async () => {
    testDb = await makeTestDb()
  })

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

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

  it('restores the debit + marks failed when the executor fails', 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 failing: PayoutExecutor = {
      execute: async () => ({ ok: false, code: 'PROVIDER_DOWN', error: 'x' }),
    }

    const result = await settlePayout(testDb, failing, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) {
      expect(result.code).toBe('PROVIDER_DOWN')
      expect(result.restored).toBe(true)
    }

    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('x')
  })

  it('marks paid + persists externalRefs immediately when the executor succeeds', 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: 1500n,
    })

    const ok: PayoutExecutor = {
      execute: async () => ({
        ok: true,
        externalRefs: { transferId: 'tr_abc', payoutId: 'po_xyz' },
      }),
    }

    const result = await settlePayout(testDb, ok, { payoutId: payout.id })

    expect(result.ok).toBe(true)
    if (result.ok) {
      expect(result.externalRefs.transferId).toBe('tr_abc')
      expect(result.externalRefs.payoutId).toBe('po_xyz')
      expect(result.ledgerEntryId).toBeTruthy()
    }

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

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

  it('is idempotent on payoutId — re-entry on an already-paid row does NOT re-call execute', 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: 1000n,
    })

    let executeCount = 0
    const counting: PayoutExecutor = {
      execute: async () => {
        executeCount += 1
        return { ok: true, externalRefs: { transferId: 'tr_first', payoutId: 'po_first' } }
      },
    }

    const first = await settlePayout(testDb, counting, { payoutId: payout.id })

    expect(first.ok).toBe(true)
    expect(executeCount).toBe(1)

    const second = await settlePayout(testDb, counting, { payoutId: payout.id })

    expect(second.ok).toBe(true)
    expect(executeCount).toBe(1)
    if (second.ok && first.ok) {
      expect(second.ledgerEntryId).toBe(first.ledgerEntryId)
      expect(second.ledgerEntryId).toBeTruthy()
      expect(second.ledgerEntryId).not.toBe('')
    }
  })

  it('does NOT hold the wallet row lock across executor.execute (executor runs outside the tx)', 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: 1200n,
    })

    let releaseExecute!: () => void
    const executeStarted = new Promise<void>((resolve) => {
      releaseExecute = resolve
    })
    let unblockExecute!: () => void
    const executeGate = new Promise<void>((resolve) => {
      unblockExecute = resolve
    })

    const blocking: PayoutExecutor = {
      execute: async () => {
        releaseExecute()
        await executeGate
        return { ok: true, externalRefs: { transferId: 'tr_lock_test' } }
      },
    }

    const settlePromise = settlePayout(testDb, blocking, { payoutId: payout.id })

    await executeStarted

    const lockStart = Date.now()
    await testDb.transaction(async (tx) => {
      await tx
        .select()
        .from(walletVesting)
        .where(eq(walletVesting.ownerId, userId))
        .for('update')
    })
    const lockMs = Date.now() - lockStart

    expect(lockMs).toBeLessThan(2000)

    unblockExecute()
    const result = await settlePromise
    expect(result.ok).toBe(true)
  })

  it('rejects terminal failed rows — never re-settles', async () => {
    const { id: userId } = await createUser(testDb)
    const enrollment = await createEnrollment(testDb, userId)
    const payout = await createPayoutRow(testDb, {
      userId,
      enrollmentId: enrollment.id,
      amountAgorot: 1000n,
      status: 'failed',
    })

    await seedOwnerCreditForPayout(testDb, userId, 5000n)

    let executeCount = 0
    const executor: PayoutExecutor = {
      execute: async () => {
        executeCount += 1
        return { ok: true, externalRefs: { transferId: 'tr_should_not_run' } }
      },
    }

    const result = await settlePayout(testDb, executor, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) {
      expect(result.code).toBe('PAYOUT_TERMINAL')
      expect(result.restored).toBe(false)
    }
    expect(executeCount).toBe(0)
  })

  it('rejects terminal cancelled rows — never re-settles', async () => {
    const { id: userId } = await createUser(testDb)
    const enrollment = await createEnrollment(testDb, userId)
    const payout = await createPayoutRow(testDb, {
      userId,
      enrollmentId: enrollment.id,
      amountAgorot: 1000n,
      status: 'cancelled',
    })

    const result = await settlePayout(testDb, {
      execute: async () => ({ ok: true, externalRefs: { transferId: 'tr_x' } }),
    }, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) expect(result.code).toBe('PAYOUT_TERMINAL')
  })

  it('rejects requested rows — only approved is claimable', 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: 1000n,
      status: 'requested',
    })

    const result = await settlePayout(testDb, {
      execute: async () => ({ ok: true, externalRefs: { transferId: 'tr_x' } }),
    }, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) expect(result.code).toBe('PAYOUT_NOT_APPROVED')
  })

  it('rejects when enrollment stripeAccountId is null — PAYOUT_NO_DESTINATION', async () => {
    const { id: userId } = await createUser(testDb)
    const enrollment = await createEnrollment(testDb, userId, { stripeAccountId: null })
    await seedOwnerCreditForPayout(testDb, userId, 5000n)
    await syncWithdrawable(testDb, userId)
    const payout = await createPayoutRow(testDb, {
      userId,
      enrollmentId: enrollment.id,
      amountAgorot: 1000n,
    })

    let executeCount = 0
    const result = await settlePayout(testDb, {
      execute: async () => {
        executeCount += 1
        return { ok: true, externalRefs: { transferId: 'tr_x' } }
      },
    }, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) expect(result.code).toBe('PAYOUT_NO_DESTINATION')
    expect(executeCount).toBe(0)
  })

  it('rejects rows already in processing — PAYOUT_IN_PROGRESS, never re-executes', async () => {
    const { id: userId } = await createUser(testDb)
    const enrollment = await createEnrollment(testDb, userId)
    const payout = await createPayoutRow(testDb, {
      userId,
      enrollmentId: enrollment.id,
      amountAgorot: 1000n,
      status: 'processing',
    })

    await seedOwnerCreditForPayout(testDb, userId, 5000n)

    let executeCount = 0
    const result = await settlePayout(testDb, {
      execute: async () => {
        executeCount += 1
        return { ok: true, externalRefs: { transferId: 'tr_x' } }
      },
    }, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) {
      expect(result.code).toBe('PAYOUT_IN_PROGRESS')
      expect(result.restored).toBe(false)
    }
    expect(executeCount).toBe(0)
  })

  it('transfer landed but stuck-sweep won the race — PAYOUT_SWEPT_DURING_EXECUTION, no double-pay', 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 sweeping: PayoutExecutor = {
      execute: async () => {
        await sweepStuckPayouts(testDb, 0)
        return { ok: true, externalRefs: { transferId: 'tr_swept', payoutId: 'po_swept' } }
      },
    }

    const result = await settlePayout(testDb, sweeping, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) {
      expect(result.code).toBe('PAYOUT_SWEPT_DURING_EXECUTION')
      expect(result.restored).toBe(true)
    }

    const payoutRow = await testDb
      .select()
      .from(affiliatePayoutsTable)
      .where(eq(affiliatePayoutsTable.id, payout.id))
    expect(payoutRow[0]!.status).toBe('failed')
    expect(payoutRow[0]!.stripeTransferId).toBe('tr_swept')
    expect(payoutRow[0]!.stripePayoutId).toBe('po_swept')

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

  it('rejects approved row with amountAgorot <= 0 — PAYOUT_INVALID_AMOUNT', async () => {
    const { id: userId } = await createUser(testDb)
    const enrollment = await createEnrollment(testDb, userId)
    const payout = await createPayoutRow(testDb, {
      userId,
      enrollmentId: enrollment.id,
      amountAgorot: 0n,
    })

    const result = await settlePayout(testDb, {
      execute: async () => ({ ok: true, externalRefs: { transferId: 'tr_x' } }),
    }, { payoutId: payout.id })

    expect(result.ok).toBe(false)
    if (!result.ok) expect(result.code).toBe('PAYOUT_INVALID_AMOUNT')
  })
})
