import { sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { TransactionalDatabase } from '@platform-modules/db'
import {
  isVoucherAlreadyRedeemedError,
  isVoucherExpiredError,
  isVoucherWrongVendorError,
} from '../errors.js'
import { issueVoucher } from './issue.js'
import { redeemVoucher } from './redeem.js'
import { startPg } from '../pg-harness.js'
import { fulfillmentSchema, type FulfillmentSchema } from '../schema.js'

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

describe('redeemVoucher (real Postgres)', () => {
  let db: TransactionalDatabase<FulfillmentSchema>
  let dbB: TransactionalDatabase<FulfillmentSchema>
  let poolB: pg.Pool | undefined
  let stop: (() => Promise<void>) | undefined

  beforeAll(async () => {
    const pgResult = await startPg()
    db = pgResult.db
    const { host, port, user, database } = pgResult.pool.options
    poolB = new pg.Pool({ host, port, user, database, max: 1 })
    dbB = drizzle(poolB, { schema: fulfillmentSchema }) as TransactionalDatabase<FulfillmentSchema>
    stop = pgResult.stop
  }, 120_000)

  afterAll(async () => {
    await poolB?.end().catch(() => undefined)
    await stop?.()
  }, 30_000)

  async function issueOne(
    orderId: string,
    overrides: Partial<Parameters<typeof issueVoucher>[1]> = {},
  ) {
    const [voucher] = await db.transaction((tx) =>
      issueVoucher(tx, {
        orderId,
        lineId: 'line-redeem',
        qty: 1,
        vendorId: 'vendor-a',
        expiresAt: new Date('2026-12-31T00:00:00.000Z'),
        ...overrides,
      }),
    )
    if (!voucher) throw new Error('expected voucher')
    return voucher
  }

  it('two concurrent redeemVoucher on same voucher: one winner, one VoucherAlreadyRedeemedError', async () => {
    const orderId = 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'
    const voucher = await issueOne(orderId)

    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) => {
      const redeemed = await redeemVoucher(txA, {
        voucherId: voucher.id,
        scanningVendorId: 'vendor-a',
      })
      txALocked()
      await hold
      return redeemed
    })

    await txAHasLock

    const txB = dbB.transaction(async (txB) => {
      return redeemVoucher(txB, {
        voucherId: voucher.id,
        scanningVendorId: 'vendor-a',
      })
    })

    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)
    expect(isVoucherAlreadyRedeemedError((rejected[0] as PromiseRejectedResult).reason)).toBe(true)

    const winner = (fulfilled[0] as PromiseFulfilledResult<Awaited<typeof txA>>).value
    expect(winner.state).toBe('REDEEMED')
    expect(winner.redeemedAt).toBeInstanceOf(Date)

    const rowRes = (await db.execute(sql`
      SELECT state, redeemed_at FROM voucher WHERE id = ${voucher.id}::uuid
    `)) as unknown as { rows?: Array<{ state: string; redeemed_at: Date }> } | Array<{
      state: string
      redeemed_at: Date
    }>
    const rows = (Array.isArray(rowRes) ? rowRes : rowRes.rows) ?? []
    expect(rows[0]?.state).toBe('REDEEMED')
    expect(rows[0]?.redeemed_at).toBeTruthy()
  })

  it('expired voucher → VoucherExpiredError', async () => {
    const orderId = 'ffffffff-ffff-4fff-8fff-ffffffffffff'
    const voucher = await issueOne(orderId, {
      expiresAt: new Date('2026-06-01T00:00:00.000Z'),
    })

    await expect(
      db.transaction((tx) =>
        redeemVoucher(tx, {
          voucherId: voucher.id,
          scanningVendorId: 'vendor-a',
        }),
      ),
    ).rejects.toSatisfy((e: unknown) => isVoucherExpiredError(e))
  })

  it('secaudit-fulfillment-redeem-server-time', async () => {
    const orderId = '55555555-5555-4555-8555-555555555555'
    const voucher = await issueOne(orderId, {
      expiresAt: new Date('2020-01-01T00:00:00.000Z'),
    })

    await expect(
      db.transaction((tx) =>
        redeemVoucher(tx, {
          voucherId: voucher.id,
          scanningVendorId: 'vendor-a',
        }),
      ),
    ).rejects.toSatisfy((e: unknown) => isVoucherExpiredError(e))

    const rowRes = await db.execute(sql`
      SELECT state FROM voucher WHERE id = ${voucher.id}::uuid
    `)
    const rows = (Array.isArray(rowRes) ? rowRes : (rowRes as { rows?: Array<{ state: string }> }).rows) ?? []
    expect(rows[0]?.state).toBe('UNREDEEMED')
  })

  it('wrong scanning vendor on vendor-scoped voucher → VoucherWrongVendorError', async () => {
    const orderId = '11111111-1111-4111-8111-111111111111'
    const voucher = await issueOne(orderId, { vendorId: 'vendor-a' })

    await expect(
      db.transaction((tx) =>
        redeemVoucher(tx, {
          voucherId: voucher.id,
          scanningVendorId: 'vendor-b',
        }),
      ),
    ).rejects.toSatisfy((e: unknown) => isVoucherWrongVendorError(e))
  })

  it('NULL vendor_id redeems for any scanning vendor', async () => {
    const orderId = '22222222-2222-4222-8222-222222222222'
    const voucher = await issueOne(orderId, { vendorId: null })

    const redeemed = await db.transaction((tx) =>
      redeemVoucher(tx, {
        voucherId: voucher.id,
        scanningVendorId: 'any-vendor',
      }),
    )

    expect(redeemed.state).toBe('REDEEMED')
    expect(redeemed.redeemedAt).toBeInstanceOf(Date)
  })
})
