import { sql } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { createPgliteClient } from '@platform-modules/db/pglite'
import {
  InventoryItemNotFoundError,
  InventoryValidationError,
  OversoldError,
  isInventoryItemNotFoundError,
  isInventoryValidationError,
  isOversoldError,
} from './errors.js'
import { pushSchema } from './migrate.js'
import { reserve } from './reserve.js'
import { inventorySchema } from './schema.js'
import { setInventory } from './set-inventory.js'

const SKU_ID = '11111111-1111-4111-8111-111111111111'
const ABSENT_SKU = '22222222-2222-4222-8222-222222222222'
const NOW = new Date('2026-06-19T12:00:00.000Z')
const TTL_MS = 15 * 60 * 1000

async function freshDb() {
  const db = createPgliteClient({ schema: inventorySchema })
  await pushSchema(db)
  return db
}

describe('reserve', () => {
  it('rejects oversell at cap (5 total, 2+2 ok, third reserve of 2 → OversoldError)', async () => {
    const db = await freshDb()
    await db.transaction((tx) => setInventory(tx, { skuId: SKU_ID, quantityTotal: 5 }))

    await db.transaction((tx) =>
      reserve(tx, { skuId: SKU_ID, qty: 2, holderRef: 'h1', now: NOW }),
    )
    await db.transaction((tx) =>
      reserve(tx, { skuId: SKU_ID, qty: 2, holderRef: 'h2', now: NOW }),
    )

    await expect(
      db.transaction((tx) =>
        reserve(tx, { skuId: SKU_ID, qty: 2, holderRef: 'h3', now: NOW }),
      ),
    ).rejects.toSatisfy((e) => isOversoldError(e))

    try {
      await db.transaction((tx) =>
        reserve(tx, { skuId: SKU_ID, qty: 2, holderRef: 'h3', now: NOW }),
      )
    } catch (e) {
      expect(e).toBeInstanceOf(OversoldError)
      expect((e as OversoldError).skuId).toBe(SKU_ID)
      expect((e as OversoldError).requested).toBe(2)
    }
  })

  it('throws InventoryItemNotFoundError when the inventory_item row is absent', async () => {
    const db = await freshDb()

    await expect(
      db.transaction((tx) =>
        reserve(tx, { skuId: ABSENT_SKU, qty: 1, holderRef: 'h1', now: NOW }),
      ),
    ).rejects.toSatisfy((e) => isInventoryItemNotFoundError(e))

    try {
      await db.transaction((tx) =>
        reserve(tx, { skuId: ABSENT_SKU, qty: 1, holderRef: 'h1', now: NOW }),
      )
    } catch (e) {
      expect(e).toBeInstanceOf(InventoryItemNotFoundError)
      expect((e as InventoryItemNotFoundError).skuId).toBe(ABSENT_SKU)
    }
  })

  it('rejects non-positive and non-integer qty with InventoryValidationError', async () => {
    const db = await freshDb()
    await db.transaction((tx) => setInventory(tx, { skuId: SKU_ID, quantityTotal: 10 }))

    for (const qty of [0, -1, 1.5]) {
      await expect(
        db.transaction((tx) =>
          reserve(tx, { skuId: SKU_ID, qty, holderRef: 'h1', now: NOW }),
        ),
      ).rejects.toSatisfy((e) => isInventoryValidationError(e))
    }
  })

  it('rejects an empty or whitespace-only holderRef with InventoryValidationError', async () => {
    const db = await freshDb()
    await db.transaction((tx) => setInventory(tx, { skuId: SKU_ID, quantityTotal: 10 }))

    for (const holderRef of ['', '   ']) {
      await expect(
        db.transaction((tx) =>
          reserve(tx, { skuId: SKU_ID, qty: 1, holderRef, now: NOW }),
        ),
      ).rejects.toSatisfy((e) => isInventoryValidationError(e))
    }

    const countRes = await db.execute(sql`
      SELECT COUNT(*)::int AS count FROM stock_reservation WHERE sku_id = ${SKU_ID}::uuid
    `)
    const rows = (Array.isArray(countRes) ? countRes : countRes.rows) as Array<{ count: number }>
    expect(Number(rows[0]?.count)).toBe(0)
  })

  it('is idempotent on (skuId, holderRef) retry — same reservationId, one row', async () => {
    const db = await freshDb()
    await db.transaction((tx) => setInventory(tx, { skuId: SKU_ID, quantityTotal: 10 }))

    const first = await db.transaction((tx) =>
      reserve(tx, { skuId: SKU_ID, qty: 2, holderRef: 'checkout-1', now: NOW }),
    )
    const second = await db.transaction((tx) =>
      reserve(tx, { skuId: SKU_ID, qty: 2, holderRef: 'checkout-1', now: NOW }),
    )

    expect(second.reservationId).toBe(first.reservationId)

    const countRes = await db.execute(sql`
      SELECT COUNT(*)::int AS count
      FROM stock_reservation
      WHERE sku_id = ${SKU_ID}::uuid
        AND holder_ref = 'checkout-1'
        AND consumed_at IS NULL
        AND released_at IS NULL
    `)
    const rows = (Array.isArray(countRes) ? countRes : countRes.rows) as Array<{ count: number }>
    expect(Number(rows[0]?.count)).toBe(1)
  })

  it('succeeds for unlimited stock (quantity_total = null) at any qty', async () => {
    const db = await freshDb()
    await db.transaction((tx) => setInventory(tx, { skuId: SKU_ID, quantityTotal: null }))

    const { reservationId } = await db.transaction((tx) =>
      reserve(tx, { skuId: SKU_ID, qty: 999, holderRef: 'h1', now: NOW }),
    )

    expect(reservationId).toMatch(/^[0-9a-f-]{36}$/i)

    const rowRes = await db.execute(sql`
      SELECT qty FROM stock_reservation WHERE id = ${reservationId}::uuid
    `)
    const rows = (Array.isArray(rowRes) ? rowRes : rowRes.rows) as Array<{ qty: number }>
    expect(Number(rows[0]?.qty)).toBe(999)
  })

  it('writes expires_at from injected now + ttlMs (never DB now())', async () => {
    const db = await freshDb()
    await db.transaction((tx) => setInventory(tx, { skuId: SKU_ID, quantityTotal: 10 }))

    const { reservationId } = await db.transaction((tx) =>
      reserve(tx, {
        skuId: SKU_ID,
        qty: 1,
        holderRef: 'ttl-hold',
        now: NOW,
        ttlMs: TTL_MS,
      }),
    )

    const expected = new Date(NOW.getTime() + TTL_MS).toISOString()
    const rowRes = await db.execute(sql`
      SELECT expires_at FROM stock_reservation WHERE id = ${reservationId}::uuid
    `)
    const rows = (Array.isArray(rowRes) ? rowRes : rowRes.rows) as Array<{ expires_at: Date | string }>
    const expiresAt = rows[0]?.expires_at
    expect(new Date(expiresAt as string | Date).toISOString()).toBe(expected)
  })
})
