import { eq, sql } from 'drizzle-orm'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { isPromoQuotaExhaustedError } from './errors.js'
import { startPg } from './pg-harness.js'
import { recordRedemption } from './record-redemption.js'
import { promo, promoUserUsage, type PromotionsSchema } from './schema.js'
import type { TransactionalDatabase } from '@platform-modules/db'

const PROMO_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
const USER_ID = 'user-11111111-1111-4111-8111-111111111111'

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

async function insertPromo(
  db: TransactionalDatabase<PromotionsSchema>,
  overrides: Partial<typeof promo.$inferInsert> = {},
): Promise<void> {
  const id = overrides.id ?? PROMO_ID
  await db.insert(promo).values({
    id,
    code: `PROMO-${id.slice(0, 8)}`,
    kind: 'percentage',
    valueBps: 1000,
    scope: { kind: 'all' },
    eligibility: {},
    funder: 'platform',
    vendorId: null,
    active: true,
    uses: 0,
    ...overrides,
  })
}

describe('recordRedemption concurrency (real Postgres)', () => {
  let db: TransactionalDatabase<PromotionsSchema>
  let dbB: TransactionalDatabase<PromotionsSchema>
  let stop: (() => Promise<void>) | undefined

  beforeAll(async () => {
    const pg = await startPg()
    db = pg.db
    dbB = pg.dbB
    stop = pg.stop
  }, 120_000)

  afterAll(async () => {
    await stop?.()
  }, 30_000)

  it('deterministic interleave on maxUses=1: one winner, loser → global quota exhausted', async () => {
    await insertPromo(db, { maxUses: 1 })

    let resolveHold!: () => void
    const hold = new Promise<void>((resolve) => {
      resolveHold = resolve
    })

    let txALocked!: () => void
    const txAHasLock = new Promise<void>((resolve) => {
      txALocked = resolve
    })

    const txA = db.transaction(async (txA) => {
      await recordRedemption(txA, PROMO_ID, USER_ID)
      txALocked()
      await hold
    })

    await txAHasLock

    const txB = dbB.transaction(async (txB) => {
      await recordRedemption(txB, PROMO_ID, USER_ID)
    })

    await sleep(400)
    resolveHold()

    const results = await Promise.allSettled([txA, txB])
    const fulfilled = results.filter((r) => r.status === 'fulfilled')
    const rejected = results.filter((r) => r.status === 'rejected')

    expect(fulfilled).toHaveLength(1)
    expect(rejected).toHaveLength(1)
    const reason = (rejected[0] as PromiseRejectedResult).reason
    expect(isPromoQuotaExhaustedError(reason)).toBe(true)
    expect((reason as { reason: string }).reason).toBe('global')

    const [row] = await db.select().from(promo).where(eq(promo.id, PROMO_ID))
    expect(row?.uses).toBe(1)
  })

  it('per-user cap=1: second redemption for same user throws per_user', async () => {
    const promoId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
    await insertPromo(db, { id: promoId, perUserCap: 1, maxUses: null })

    await db.transaction((tx) => recordRedemption(tx, promoId, USER_ID))

    await expect(db.transaction((tx) => recordRedemption(tx, promoId, USER_ID))).rejects.toSatisfy(
      (e) => isPromoQuotaExhaustedError(e) && (e as { reason: string }).reason === 'per_user',
    )

    const usage = await db
      .select()
      .from(promoUserUsage)
      .where(eq(promoUserUsage.promoId, promoId))
    expect(usage).toHaveLength(1)
    expect(usage[0]?.uses).toBe(1)
  })

  it('per-user cap=2: third redemption throws per_user (not a unique-constraint cap-of-1)', async () => {
    const promoId = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'
    await insertPromo(db, { id: promoId, perUserCap: 2, maxUses: null })

    await db.transaction((tx) => recordRedemption(tx, promoId, USER_ID))
    await db.transaction((tx) => recordRedemption(tx, promoId, USER_ID))

    await expect(db.transaction((tx) => recordRedemption(tx, promoId, USER_ID))).rejects.toSatisfy(
      (e) => isPromoQuotaExhaustedError(e) && (e as { reason: string }).reason === 'per_user',
    )

    const usage = await db
      .select()
      .from(promoUserUsage)
      .where(eq(promoUserUsage.promoId, promoId))
    expect(usage[0]?.uses).toBe(2)
  })

  it('userId=null skips per-user gate and only global maxUses applies', async () => {
    const promoId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'
    await insertPromo(db, { id: promoId, perUserCap: 1, maxUses: 2 })

    await db.transaction((tx) => recordRedemption(tx, promoId, null))
    await db.transaction((tx) => recordRedemption(tx, promoId, null))

    const usage = await db
      .select()
      .from(promoUserUsage)
      .where(eq(promoUserUsage.promoId, promoId))
    expect(usage).toHaveLength(0)

    const [row] = await db.select().from(promo).where(eq(promo.id, promoId))
    expect(row?.uses).toBe(2)

    await expect(db.transaction((tx) => recordRedemption(tx, promoId, null))).rejects.toSatisfy(
      (e) => isPromoQuotaExhaustedError(e) && (e as { reason: string }).reason === 'global',
    )
  })

  it('uncapped promo: N concurrent redemptions all succeed', async () => {
    const promoId = 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'
    const n = 20
    await insertPromo(db, { id: promoId, maxUses: null, perUserCap: null })

    const results = await Promise.allSettled(
      Array.from({ length: n }, (_, i) =>
        db.transaction((tx) => recordRedemption(tx, promoId, `user-${i}`)),
      ),
    )

    const fulfilled = results.filter((r) => r.status === 'fulfilled')
    expect(fulfilled).toHaveLength(n)

    const countRes = (await db.execute(sql`
      SELECT uses::int AS uses FROM promo WHERE id = ${promoId}::uuid
    `)) as unknown as { rows?: Array<{ uses: number }> } | Array<{ uses: number }>
    const rows = (Array.isArray(countRes) ? countRes : countRes.rows) ?? []
    expect(Number(rows[0]?.uses)).toBe(n)
  })
})
